1use serde::Deserialize;
2use std::collections::HashSet;
3use std::rc::Rc;
4
5use once_cell::sync::Lazy;
6use regex::Regex;
7use tree_sitter::Node;
8
9use crate::{
10 linter::{range_from_tree_sitter, RuleViolation},
11 rules::{Context, Rule, RuleLinter, RuleType},
12};
13
14#[derive(Debug, PartialEq, Clone, Deserialize)]
16pub struct MD054LinkImageStyleTable {
17 #[serde(default)]
18 pub autolink: bool,
19 #[serde(default)]
20 pub inline: bool,
21 #[serde(default)]
22 pub full: bool,
23 #[serde(default)]
24 pub collapsed: bool,
25 #[serde(default)]
26 pub shortcut: bool,
27 #[serde(default)]
28 pub url_inline: bool,
29}
30
31impl Default for MD054LinkImageStyleTable {
32 fn default() -> Self {
33 Self {
34 autolink: true,
35 inline: true,
36 full: true,
37 collapsed: true,
38 shortcut: true,
39 url_inline: true,
40 }
41 }
42}
43
44static RE_INLINE: Lazy<Regex> = Lazy::new(|| {
50 Regex::new(r"(!\[([^\]]*)\]\(([^)]*)\))|((?:^|[^!])\[([^\]]*)\]\(([^)]*)\))").unwrap()
51});
52
53static RE_FULL_REFERENCE: Lazy<Regex> = Lazy::new(|| {
55 Regex::new(r"(!\[([^\]]*)\]\[([^\]]+)\])|((?:^|[^!])\[([^\]]*)\]\[([^\]]+)\])").unwrap()
56});
57
58static RE_COLLAPSED_REFERENCE: Lazy<Regex> =
60 Lazy::new(|| Regex::new(r"(!\[([^\]]+)\]\[\])|((?:^|[^!])\[([^\]]+)\]\[\])").unwrap());
61
62static RE_SHORTCUT_REFERENCE: Lazy<Regex> =
64 Lazy::new(|| Regex::new(r"(!\[([^\]]+)\])|((?:^|[^!])\[([^\]]+)\])").unwrap());
65
66static RE_AUTOLINK: Lazy<Regex> = Lazy::new(|| Regex::new(r"<(https?://[^>]+)>").unwrap());
68
69pub(crate) struct MD054Linter {
73 context: Rc<Context>,
74 violations: Vec<RuleViolation>,
75}
76
77impl MD054Linter {
78 pub fn new(context: Rc<Context>) -> Self {
79 Self {
80 context,
81 violations: Vec::new(),
82 }
83 }
84}
85
86impl RuleLinter for MD054Linter {
87 fn feed(&mut self, node: &Node) {
88 if node.kind() == "inline" {
89 self.check_inline_content(node);
90 }
91 }
92
93 fn finalize(&mut self) -> Vec<RuleViolation> {
94 std::mem::take(&mut self.violations)
95 }
96}
97
98impl MD054Linter {
99 fn check_inline_content(&mut self, node: &Node) {
100 let content = {
101 let document_content = self.context.document_content.borrow();
102 node.utf8_text(document_content.as_bytes())
103 .unwrap_or("")
104 .to_string()
105 };
106
107 if !content.is_empty() {
108 self.check_content_for_violations(&content, node);
109 }
110 }
111
112 fn check_content_for_violations(&mut self, content: &str, node: &Node) {
113 let config = self
114 .context
115 .config
116 .linters
117 .settings
118 .link_image_style
119 .clone();
120 let mut found_violations = HashSet::new();
121
122 if !config.autolink {
124 for caps in RE_AUTOLINK.captures_iter(content) {
125 let start = caps.get(0).unwrap().start();
126 if found_violations.insert(("autolink", start)) {
127 self.create_violation_at_offset(
128 node,
129 content,
130 start,
131 "Autolinks are not allowed".to_string(),
132 );
133 }
134 }
135 }
136
137 for caps in RE_INLINE.captures_iter(content) {
139 if let Some(image_match) = caps.get(1) {
141 if !config.inline && found_violations.insert(("inline_image", image_match.start()))
142 {
143 self.create_violation_at_offset(
144 node,
145 content,
146 image_match.start(),
147 "Inline images are not allowed".to_string(),
148 );
149 }
150 }
151 else if let Some(link_match) = caps.get(4) {
153 let mut start = link_match.start();
154 if !link_match.as_str().starts_with('[') {
155 start += 1; }
157
158 if !config.inline {
159 if found_violations.insert(("inline_link", start)) {
160 self.create_violation_at_offset(
161 node,
162 content,
163 start,
164 "Inline links are not allowed".to_string(),
165 );
166 }
167 continue; }
169
170 if !config.url_inline {
172 if let (Some(text), Some(url)) = (caps.get(5), caps.get(6)) {
173 if text.as_str() == url.as_str()
174 && found_violations.insert(("url_inline", start))
175 {
176 self.create_violation_at_offset(
177 node,
178 content,
179 start,
180 "Inline links with matching URL text are not allowed".to_string(),
181 );
182 }
183 }
184 }
185 }
186 }
187
188 if !config.full {
190 for caps in RE_FULL_REFERENCE.captures_iter(content) {
191 if let Some(image_match) = caps.get(1) {
192 if found_violations.insert(("full_image", image_match.start())) {
193 self.create_violation_at_offset(
194 node,
195 content,
196 image_match.start(),
197 "Full reference images are not allowed".to_string(),
198 );
199 }
200 } else if let Some(link_match) = caps.get(4) {
201 let mut start = link_match.start();
202 if !link_match.as_str().starts_with('[') {
203 start += 1;
204 }
205 if found_violations.insert(("full_link", start)) {
206 self.create_violation_at_offset(
207 node,
208 content,
209 start,
210 "Full reference links are not allowed".to_string(),
211 );
212 }
213 }
214 }
215 }
216
217 if !config.collapsed {
219 for caps in RE_COLLAPSED_REFERENCE.captures_iter(content) {
220 if let Some(image_match) = caps.get(1) {
221 if found_violations.insert(("collapsed_image", image_match.start())) {
222 self.create_violation_at_offset(
223 node,
224 content,
225 image_match.start(),
226 "Collapsed reference images are not allowed".to_string(),
227 );
228 }
229 } else if let Some(link_match) = caps.get(3) {
230 let mut start = link_match.start();
231 if !link_match.as_str().starts_with('[') {
232 start += 1;
233 }
234 if found_violations.insert(("collapsed_link", start)) {
235 self.create_violation_at_offset(
236 node,
237 content,
238 start,
239 "Collapsed reference links are not allowed".to_string(),
240 );
241 }
242 }
243 }
244 }
245
246 if !config.shortcut {
248 for caps in RE_SHORTCUT_REFERENCE.captures_iter(content) {
249 let whole_match = caps.get(0).unwrap();
250 let end_offset = whole_match.end();
251
252 if end_offset < content.len() {
254 if let Some(next_char) = content[end_offset..].chars().next() {
255 if next_char == '(' || next_char == '[' {
256 continue;
257 }
258 }
259 }
260
261 if let Some(image_match) = caps.get(1) {
262 if found_violations.insert(("shortcut_image", image_match.start())) {
263 self.create_violation_at_offset(
264 node,
265 content,
266 image_match.start(),
267 "Shortcut reference images are not allowed".to_string(),
268 );
269 }
270 } else if let Some(link_match) = caps.get(3) {
271 let mut start = link_match.start();
272 if !link_match.as_str().starts_with('[') {
273 start += 1;
274 }
275 if found_violations.insert(("shortcut_link", start)) {
276 self.create_violation_at_offset(
277 node,
278 content,
279 start,
280 "Shortcut reference links are not allowed".to_string(),
281 );
282 }
283 }
284 }
285 }
286 }
287
288 fn create_violation_at_offset(
289 &mut self,
290 node: &Node,
291 content: &str,
292 offset: usize,
293 message: String,
294 ) {
295 let lines_before_offset = content[..offset].matches('\n').count();
297 let node_start_row = node.start_position().row;
298 let violation_row = node_start_row + lines_before_offset;
299
300 let violation_range = tree_sitter::Range {
302 start_byte: node.start_byte() + offset,
303 end_byte: node.start_byte() + offset + 1, start_point: tree_sitter::Point {
305 row: violation_row,
306 column: if lines_before_offset == 0 {
307 node.start_position().column + offset
308 } else {
309 let line_start = content[..offset].rfind('\n').map(|i| i + 1).unwrap_or(0);
311 offset - line_start
312 },
313 },
314 end_point: tree_sitter::Point {
315 row: violation_row,
316 column: if lines_before_offset == 0 {
317 node.start_position().column + offset + 1
318 } else {
319 let line_start = content[..offset].rfind('\n').map(|i| i + 1).unwrap_or(0);
320 offset - line_start + 1
321 },
322 },
323 };
324
325 self.violations.push(RuleViolation::new(
326 &MD054,
327 message,
328 self.context.file_path.clone(),
329 range_from_tree_sitter(&violation_range),
330 ));
331 }
332}
333
334pub const MD054: Rule = Rule {
335 id: "MD054",
336 alias: "link-image-style",
337 tags: &["links", "images"],
338 description: "Link and image style",
339 rule_type: RuleType::Token,
340 required_nodes: &["inline"],
341 new_linter: |context| Box::new(MD054Linter::new(context)),
342};
343
344#[cfg(test)]
345mod test {
346 use std::path::PathBuf;
347
348 use crate::config::{MD054LinkImageStyleTable, RuleSeverity};
349 use crate::linter::MultiRuleLinter;
350 use crate::test_utils::test_helpers::test_config_with_rules;
351
352 fn test_config() -> crate::config::QuickmarkConfig {
353 test_config_with_rules(vec![
354 ("link-image-style", RuleSeverity::Error),
355 ("heading-style", RuleSeverity::Off),
356 ("heading-increment", RuleSeverity::Off),
357 ("line-length", RuleSeverity::Off),
358 ])
359 }
360
361 fn test_config_with_settings(
362 settings: MD054LinkImageStyleTable,
363 ) -> crate::config::QuickmarkConfig {
364 let mut config = test_config();
365 config.linters.settings.link_image_style = settings;
366 config
367 }
368
369 #[test]
371 fn test_autolink_allowed() {
372 let input = "<https://example.com>";
373 let config = test_config_with_settings(MD054LinkImageStyleTable {
374 autolink: true,
375 ..Default::default()
376 });
377 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
378 let violations = linter.analyze();
379 assert_eq!(0, violations.len());
380 }
381
382 #[test]
383 fn test_autolink_disallowed() {
384 let input = "<https://example.com>";
385 let config = test_config_with_settings(MD054LinkImageStyleTable {
386 autolink: false,
387 ..Default::default()
388 });
389 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
390 let violations = linter.analyze();
391 assert_eq!(1, violations.len());
392 let violation = &violations[0];
393 assert_eq!("MD054", violation.rule().id);
394 assert!(violation.message().to_lowercase().contains("autolink"));
395 }
396
397 #[test]
399 fn test_inline_link_allowed() {
400 let input = "[example](https://example.com)";
401 let config = test_config_with_settings(MD054LinkImageStyleTable {
402 inline: true,
403 ..Default::default()
404 });
405 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
406 let violations = linter.analyze();
407 assert_eq!(0, violations.len());
408 }
409
410 #[test]
411 fn test_inline_link_disallowed() {
412 let input = "[example](https://example.com)";
413 let config = test_config_with_settings(MD054LinkImageStyleTable {
414 inline: false,
415 ..Default::default()
416 });
417 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
418 let violations = linter.analyze();
419 assert_eq!(1, violations.len());
420 let violation = &violations[0];
421 assert_eq!("MD054", violation.rule().id);
422 assert!(violation.message().to_lowercase().contains("inline"));
423 }
424
425 #[test]
427 fn test_inline_image_allowed() {
428 let input = "";
429 let config = test_config_with_settings(MD054LinkImageStyleTable {
430 inline: true,
431 ..Default::default()
432 });
433 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
434 let violations = linter.analyze();
435 assert_eq!(0, violations.len());
436 }
437
438 #[test]
439 fn test_inline_image_disallowed() {
440 let input = "";
441 let config = test_config_with_settings(MD054LinkImageStyleTable {
442 inline: false,
443 ..Default::default()
444 });
445 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
446 let violations = linter.analyze();
447 assert_eq!(1, violations.len());
448 let violation = &violations[0];
449 assert_eq!("MD054", violation.rule().id);
450 assert!(violation.message().to_lowercase().contains("inline"));
451 }
452
453 #[test]
455 fn test_full_reference_link_allowed() {
456 let input = "[example][ref]\n\n[ref]: https://example.com";
457 let config = test_config_with_settings(MD054LinkImageStyleTable {
458 full: true,
459 ..Default::default()
460 });
461 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
462 let violations = linter.analyze();
463 assert_eq!(0, violations.len());
464 }
465
466 #[test]
467 fn test_full_reference_link_disallowed() {
468 let input = "[example][ref]\n\n[ref]: https://example.com";
469 let config = test_config_with_settings(MD054LinkImageStyleTable {
470 full: false,
471 ..Default::default()
472 });
473 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
474 let violations = linter.analyze();
475 assert_eq!(1, violations.len());
476 let violation = &violations[0];
477 assert_eq!("MD054", violation.rule().id);
478 assert!(violation
479 .message()
480 .to_lowercase()
481 .contains("full reference"));
482 }
483
484 #[test]
486 fn test_full_reference_image_allowed() {
487 let input = "![alt text][ref]\n\n[ref]: https://example.com/image.jpg";
488 let config = test_config_with_settings(MD054LinkImageStyleTable {
489 full: true,
490 ..Default::default()
491 });
492 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
493 let violations = linter.analyze();
494 assert_eq!(0, violations.len());
495 }
496
497 #[test]
498 fn test_full_reference_image_disallowed() {
499 let input = "![alt text][ref]\n\n[ref]: https://example.com/image.jpg";
500 let config = test_config_with_settings(MD054LinkImageStyleTable {
501 full: false,
502 ..Default::default()
503 });
504 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
505 let violations = linter.analyze();
506 assert_eq!(1, violations.len());
507 let violation = &violations[0];
508 assert_eq!("MD054", violation.rule().id);
509 assert!(violation
510 .message()
511 .to_lowercase()
512 .contains("full reference"));
513 }
514
515 #[test]
517 fn test_collapsed_reference_link_allowed() {
518 let input = "[example][]\n\n[example]: https://example.com";
519 let config = test_config_with_settings(MD054LinkImageStyleTable {
520 collapsed: true,
521 ..Default::default()
522 });
523 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
524 let violations = linter.analyze();
525 assert_eq!(0, violations.len());
526 }
527
528 #[test]
529 fn test_collapsed_reference_link_disallowed() {
530 let input = "[example][]\n\n[example]: https://example.com";
531 let config = test_config_with_settings(MD054LinkImageStyleTable {
532 collapsed: false,
533 ..Default::default()
534 });
535 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
536 let violations = linter.analyze();
537 assert_eq!(1, violations.len());
538 let violation = &violations[0];
539 assert_eq!("MD054", violation.rule().id);
540 assert!(violation
541 .message()
542 .to_lowercase()
543 .contains("collapsed reference"));
544 }
545
546 #[test]
548 fn test_collapsed_reference_image_allowed() {
549 let input = "![example][]\n\n[example]: https://example.com/image.jpg";
550 let config = test_config_with_settings(MD054LinkImageStyleTable {
551 collapsed: true,
552 ..Default::default()
553 });
554 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
555 let violations = linter.analyze();
556 assert_eq!(0, violations.len());
557 }
558
559 #[test]
560 fn test_collapsed_reference_image_disallowed() {
561 let input = "![example][]\n\n[example]: https://example.com/image.jpg";
562 let config = test_config_with_settings(MD054LinkImageStyleTable {
563 collapsed: false,
564 ..Default::default()
565 });
566 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
567 let violations = linter.analyze();
568 assert_eq!(1, violations.len());
569 let violation = &violations[0];
570 assert_eq!("MD054", violation.rule().id);
571 assert!(violation
572 .message()
573 .to_lowercase()
574 .contains("collapsed reference"));
575 }
576
577 #[test]
579 fn test_shortcut_reference_link_allowed() {
580 let input = "[example]\n\n[example]: https://example.com";
581 let config = test_config_with_settings(MD054LinkImageStyleTable {
582 shortcut: true,
583 ..Default::default()
584 });
585 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
586 let violations = linter.analyze();
587 assert_eq!(0, violations.len());
588 }
589
590 #[test]
591 fn test_shortcut_reference_link_disallowed() {
592 let input = "[example]\n\n[example]: https://example.com";
593 let config = test_config_with_settings(MD054LinkImageStyleTable {
594 shortcut: false,
595 ..Default::default()
596 });
597 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
598 let violations = linter.analyze();
599 assert_eq!(1, violations.len());
600 let violation = &violations[0];
601 assert_eq!("MD054", violation.rule().id);
602 assert!(violation
603 .message()
604 .to_lowercase()
605 .contains("shortcut reference"));
606 }
607
608 #[test]
610 fn test_shortcut_reference_image_allowed() {
611 let input = "![example]\n\n[example]: https://example.com/image.jpg";
612 let config = test_config_with_settings(MD054LinkImageStyleTable {
613 shortcut: true,
614 ..Default::default()
615 });
616 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
617 let violations = linter.analyze();
618 assert_eq!(0, violations.len());
619 }
620
621 #[test]
622 fn test_shortcut_reference_image_disallowed() {
623 let input = "![example]\n\n[example]: https://example.com/image.jpg";
624 let config = test_config_with_settings(MD054LinkImageStyleTable {
625 shortcut: false,
626 ..Default::default()
627 });
628 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
629 let violations = linter.analyze();
630 assert_eq!(1, violations.len());
631 let violation = &violations[0];
632 assert_eq!("MD054", violation.rule().id);
633 assert!(violation
634 .message()
635 .to_lowercase()
636 .contains("shortcut reference"));
637 }
638
639 #[test]
641 fn test_url_inline_link_allowed() {
642 let input = "[https://example.com](https://example.com)";
643 let config = test_config_with_settings(MD054LinkImageStyleTable {
644 url_inline: true,
645 ..Default::default()
646 });
647 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
648 let violations = linter.analyze();
649 assert_eq!(0, violations.len());
650 }
651
652 #[test]
653 fn test_url_inline_link_disallowed() {
654 let input = "[https://example.com](https://example.com)";
655 let config = test_config_with_settings(MD054LinkImageStyleTable {
656 url_inline: false,
657 ..Default::default()
658 });
659 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
660 let violations = linter.analyze();
661 assert_eq!(1, violations.len());
662 let violation = &violations[0];
663 assert_eq!("MD054", violation.rule().id);
664 assert!(violation
665 .message()
666 .to_lowercase()
667 .contains("matching url text"));
668 }
669
670 #[test]
672 fn test_multiple_styles_disallowed() {
673 let input = r#"
674[inline link](https://example.com)
675<https://example.com>
676[reference][ref]
677
678[ref]: https://example.com
679"#;
680 let config = test_config_with_settings(MD054LinkImageStyleTable {
681 autolink: false,
682 inline: false,
683 full: false,
684 collapsed: true,
685 shortcut: true,
686 url_inline: true,
687 });
688 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
689 let violations = linter.analyze();
690 assert_eq!(3, violations.len()); for violation in &violations {
692 assert_eq!("MD054", violation.rule().id);
693 }
694 }
695
696 #[test]
698 fn test_all_styles_allowed() {
699 let input = r#"
700[inline link](https://example.com)
701<https://example.com>
702[reference][ref]
703[collapsed][]
704[shortcut]
705[https://example.com](https://example.com)
706
707[ref]: https://example.com
708[collapsed]: https://example.com
709[shortcut]: https://example.com
710"#;
711 let config = test_config(); let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
713 let violations = linter.analyze();
714 assert_eq!(0, violations.len());
715 }
716
717 #[test]
719 fn test_line_numbers_multiline_content() {
720 let input = "Here is some text.\n\n[Link 1](https://example.com)\n\nSome more text here.\n\n[Link 2](https://github.com)";
721 let config = test_config_with_settings(MD054LinkImageStyleTable {
722 inline: false,
723 ..Default::default()
724 });
725 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
726 let violations = linter.analyze();
727 assert_eq!(2, violations.len());
728
729 assert_eq!(2, violations[0].location().range.start.line); assert_eq!(6, violations[1].location().range.start.line); }
733
734 #[test]
736 fn test_bracket_offset_with_preceding_text() {
737 let input = "Text [link](url) more text";
738 let config = test_config_with_settings(MD054LinkImageStyleTable {
739 inline: false,
740 ..Default::default()
741 });
742 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
743 let violations = linter.analyze();
744 assert_eq!(1, violations.len());
745
746 assert_eq!(5, violations[0].location().range.start.character); }
749
750 #[test]
752 fn test_newline_before_link() {
753 let input = "\n[Link text](https://example.com)\n[GitHub](https://github.com)";
754 let config = test_config_with_settings(MD054LinkImageStyleTable {
755 inline: false,
756 ..Default::default()
757 });
758 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
759 let violations = linter.analyze();
760 assert_eq!(2, violations.len());
761
762 assert_eq!(1, violations[0].location().range.start.line); assert_eq!(2, violations[1].location().range.start.line); }
766
767 #[test]
769 fn test_multiple_links_same_line() {
770 let input = "[Link1](url1) and [Link2](url2)";
771 let config = test_config_with_settings(MD054LinkImageStyleTable {
772 inline: false,
773 ..Default::default()
774 });
775 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
776 let violations = linter.analyze();
777 assert_eq!(2, violations.len());
778
779 assert_eq!(0, violations[0].location().range.start.line);
781 assert_eq!(0, violations[1].location().range.start.line);
782 }
783
784 #[test]
786 fn test_reference_link_bracket_offset() {
787 let input = "Text [ref link][reference] more";
788 let config = test_config_with_settings(MD054LinkImageStyleTable {
789 full: false,
790 ..Default::default()
791 });
792 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
793 let violations = linter.analyze();
794 assert_eq!(1, violations.len());
795
796 assert!(violations[0].message().contains("Full reference"));
798 }
799
800 #[test]
802 fn test_collapsed_reference_bracket_offset() {
803 let input = "Text [collapsed][] more text";
804 let config = test_config_with_settings(MD054LinkImageStyleTable {
805 collapsed: false,
806 ..Default::default()
807 });
808 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
809 let violations = linter.analyze();
810 assert_eq!(1, violations.len());
811
812 assert!(violations[0].message().contains("Collapsed reference"));
814 }
815
816 #[test]
818 fn test_shortcut_reference_bracket_offset() {
819 let input = "Text [shortcut] more text\n\n[shortcut]: https://example.com";
820 let config = test_config_with_settings(MD054LinkImageStyleTable {
821 shortcut: false,
822 ..Default::default()
823 });
824 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
825 let violations = linter.analyze();
826 assert_eq!(1, violations.len());
827
828 assert!(violations[0].message().contains("Shortcut reference"));
830 }
831
832 #[test]
834 fn test_regex_non_bracket_start() {
835 let input = "!\n[Link after exclamation](url)";
836 let config = test_config_with_settings(MD054LinkImageStyleTable {
837 inline: false,
838 ..Default::default()
839 });
840 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
841 let violations = linter.analyze();
842 assert_eq!(1, violations.len());
843
844 assert_eq!(1, violations[0].location().range.start.line);
846 }
847
848 #[test]
850 fn test_parity_line_numbers() {
851 let input = "# MD054 Violations Test Cases\n\nThis file contains examples.\n\n<https://example.com>\n[Link text](https://example.com)";
853 let config = test_config_with_settings(MD054LinkImageStyleTable {
854 autolink: false,
855 inline: false,
856 ..Default::default()
857 });
858 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
859 let violations = linter.analyze();
860 assert_eq!(2, violations.len());
861
862 assert_eq!(4, violations[0].location().range.start.line); assert_eq!(5, violations[1].location().range.start.line); }
866
867 #[test]
869 fn test_image_bracket_offset() {
870 let input = "Text  more text";
871 let config = test_config_with_settings(MD054LinkImageStyleTable {
872 inline: false,
873 ..Default::default()
874 });
875 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
876 let violations = linter.analyze();
877 assert_eq!(1, violations.len());
878
879 assert!(violations[0].message().contains("Inline images"));
881 assert_eq!(0, violations[0].location().range.start.line);
882 }
883}