1use std::{cell::RefCell, ops::Range};
2
3use crop::RopeSlice;
4use log::debug;
5use markdown::mdast::{Node, Text};
6use regex::Regex;
7use supa_mdx_macros::RuleName;
8
9use crate::{
10 context::Context,
11 errors::{LintError, LintLevel},
12 fix::{LintCorrection, LintCorrectionReplace},
13 location::{AdjustedOffset, AdjustedRange, DenormalizedLocation},
14 utils::{
15 mdast::HasChildren,
16 words::{Capitalize, CapitalizeTriggerPunctuation, WordIterator, WordIteratorOptions},
17 },
18};
19
20use super::{RegexBeginning, RegexEnding, RegexSettings, Rule, RuleName, RuleSettings};
21
22#[derive(Debug, RuleName)]
46pub struct Rule001HeadingCase {
47 may_uppercase: Vec<Regex>,
48 may_lowercase: Vec<Regex>,
49 next_word_capital: RefCell<Capitalize>,
50}
51
52impl Default for Rule001HeadingCase {
53 fn default() -> Self {
54 Self {
55 may_uppercase: Vec::new(),
56 may_lowercase: Vec::new(),
57 next_word_capital: RefCell::new(Capitalize::True),
58 }
59 }
60}
61
62impl Rule for Rule001HeadingCase {
63 fn default_level(&self) -> LintLevel {
64 LintLevel::Error
65 }
66
67 fn setup(&mut self, settings: Option<&mut RuleSettings>) {
68 if let Some(settings) = settings {
69 let regex_settings = RegexSettings {
70 beginning: Some(RegexBeginning::VeryBeginning),
71 ending: Some(RegexEnding::WordBoundary),
72 };
73
74 if let Some(vec) = settings.get_array_of_regexes("may_uppercase", Some(®ex_settings))
75 {
76 self.may_uppercase = vec;
77 }
78 if let Some(vec) = settings.get_array_of_regexes("may_lowercase", Some(®ex_settings))
79 {
80 self.may_lowercase = vec;
81 }
82 }
83 }
84
85 fn check(&self, ast: &Node, context: &Context, level: LintLevel) -> Option<Vec<LintError>> {
86 if !matches!(ast, Node::Heading(_)) {
87 return None;
88 };
89
90 self.reset_mutable_state();
91
92 let mut fixes: Option<Vec<LintCorrection>> = None;
93 self.check_ast(ast, &mut fixes, context);
94 fixes
95 .and_then(|fixes| {
96 LintError::from_node()
97 .node(ast)
98 .context(context)
99 .rule(self.name())
100 .level(level)
101 .message(&self.message())
102 .fix(fixes)
103 .call()
104 })
105 .map(|error| vec![error])
106 }
107}
108
109impl Rule001HeadingCase {
110 fn message(&self) -> String {
111 "Heading should be sentence case".to_string()
112 }
113
114 fn reset_mutable_state(&self) {
115 self.next_word_capital.replace(Capitalize::True);
116 }
117
118 fn check_text_sentence_case(
119 &self,
120 text: &Text,
121 fixes: &mut Option<Vec<LintCorrection>>,
122 context: &Context,
123 ) {
124 if let Some(position) = text.position.as_ref() {
125 let range = AdjustedRange::from_unadjusted_position(position, context);
126 let rope = context.rope().byte_slice(Into::<Range<usize>>::into(range));
127
128 let mut word_iterator = WordIterator::new(
129 rope,
130 0,
131 WordIteratorOptions {
132 initial_capitalize: *self.next_word_capital.borrow(),
133 capitalize_trigger_punctuation: CapitalizeTriggerPunctuation::PlusColon,
134 ..Default::default()
135 },
136 );
137
138 let mut first_word = *self.next_word_capital.borrow() == Capitalize::True;
139
140 while let Some((offset, word, cap)) = word_iterator.next() {
141 debug!("Got next word: {word:?} at offset {offset} with capitalization {cap:?}");
142 if word.is_empty() {
143 continue;
144 }
145
146 match cap {
147 Capitalize::True => {
148 if word.chars().next().unwrap().is_lowercase()
149 && !self.handle_exception_match(
150 rope.byte_slice(offset..),
151 offset,
152 cap,
153 &mut word_iterator,
154 )
155 {
156 self.create_text_lint_fix(
157 word.to_string(),
158 text,
159 offset,
160 cap,
161 context,
162 fixes,
163 );
164 } else if first_word {
165 self.handle_exception_match(
166 rope.byte_slice(offset..),
167 offset,
168 Capitalize::False,
169 &mut word_iterator,
170 );
171 }
172 }
173 Capitalize::False => {
174 if word.chars().next().unwrap().is_uppercase()
175 && !self.handle_exception_match(
176 rope.byte_slice(offset..),
177 offset,
178 cap,
179 &mut word_iterator,
180 )
181 {
182 self.create_text_lint_fix(
183 word.to_string(),
184 text,
185 offset,
186 cap,
187 context,
188 fixes,
189 );
190 }
191 }
192 }
193
194 first_word = false;
195 self.next_word_capital
196 .replace(word_iterator.next_capitalize().unwrap());
197 }
198 }
199 }
200
201 fn handle_exception_match(
202 &self,
203 rope: RopeSlice<'_>,
204 offset: usize,
205 capitalize: Capitalize,
206 word_iterator: &mut WordIterator<'_>,
207 ) -> bool {
208 let patterns = match capitalize {
209 Capitalize::True => &self.may_lowercase,
210 Capitalize::False => &self.may_uppercase,
211 };
212
213 let text = rope.to_string();
214 debug!("Checking for exceptions in {text}");
215 for pattern in patterns {
216 if let Some(match_result) = pattern.find(&text) {
217 debug!("Found exception match: {match_result:?}");
218 while offset + match_result.len()
219 > word_iterator
220 .curr_index()
221 .expect("WordIterator index should not be queried while unstable")
222 {
223 if word_iterator.next().is_none() {
224 break;
225 }
226 }
227
228 return true;
229 }
230 }
231
232 false
233 }
234
235 fn create_text_lint_fix(
236 &self,
237 word: String,
238 node: &Text,
239 offset: usize,
240 capitalize: Capitalize,
241 context: &Context,
242 fixes: &mut Option<Vec<LintCorrection>>,
243 ) {
244 let replacement_word = match capitalize {
245 Capitalize::True => {
246 let mut chars = word.chars();
247 let first_char = chars.next().unwrap();
248 first_char.to_uppercase().collect::<String>() + chars.as_str()
249 }
250 Capitalize::False => word.to_lowercase(),
251 };
252
253 let start_point = node
254 .position
255 .as_ref()
256 .map(|p| AdjustedOffset::from_unist(&p.start, context.content_start_offset()))
257 .map(|mut p| {
258 p.increment(offset);
259 p
260 });
261 let end_point = start_point.map(|mut p| {
262 p.increment(word.len());
263 p
264 });
265
266 if let (Some(start), Some(end)) = (start_point, end_point) {
267 let location = AdjustedRange::new(start, end);
268 let location = DenormalizedLocation::from_offset_range(location, context);
269
270 let fix = LintCorrection::Replace(LintCorrectionReplace {
271 location,
272 text: replacement_word,
273 });
274 fixes.get_or_insert_with(Vec::new).push(fix);
275 }
276 }
277
278 fn check_ast(&self, node: &Node, fixes: &mut Option<Vec<LintCorrection>>, context: &Context) {
279 debug!(
280 "Checking ast for node: {node:?} with next word capital: {:?}",
281 self.next_word_capital
282 );
283
284 fn check_children<T: HasChildren>(
285 rule: &Rule001HeadingCase,
286 node: &T,
287 fixes: &mut Option<Vec<LintCorrection>>,
288 context: &Context,
289 ) {
290 node.get_children()
291 .iter()
292 .for_each(|child| rule.check_ast(child, fixes, context));
293 }
294
295 match node {
296 Node::Text(text) => self.check_text_sentence_case(text, fixes, context),
297 Node::Emphasis(emphasis) => check_children(self, emphasis, fixes, context),
298 Node::Link(link) => check_children(self, link, fixes, context),
299 Node::LinkReference(link_reference) => {
300 check_children(self, link_reference, fixes, context)
301 }
302 Node::Strong(strong) => check_children(self, strong, fixes, context),
303 Node::Heading(heading) => check_children(self, heading, fixes, context),
304 Node::InlineCode(_) => {
305 self.next_word_capital.replace(Capitalize::False);
306 }
307 _ => {}
308 }
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use crate::parser::parse;
315
316 use super::*;
317
318 #[test]
319 fn test_rule001_correct_sentence_case() {
320 let rule = Rule001HeadingCase::default();
321 let mdx = "# This is a correct heading";
322 let parse_result = parse(mdx).unwrap();
323 let context = Context::builder()
324 .parse_result(&parse_result)
325 .build()
326 .unwrap();
327
328 let result = rule.check(
329 parse_result.ast().children().unwrap().first().unwrap(),
330 &context,
331 LintLevel::Error,
332 );
333 assert!(result.is_none());
334 }
335
336 #[test]
337 fn test_rule001_lowercase_first_word() {
338 let rule = Rule001HeadingCase::default();
339 let mdx = "# this should fail";
340 let parse_result = parse(mdx).unwrap();
341 let context = Context::builder()
342 .parse_result(&parse_result)
343 .build()
344 .unwrap();
345
346 let result = rule.check(
347 parse_result.ast().children().unwrap().first().unwrap(),
348 &context,
349 LintLevel::Error,
350 );
351 assert!(result.is_some());
352
353 let errors = result.unwrap();
354 assert_eq!(errors.len(), 1);
355
356 let fixes = errors.get(0).unwrap().fix.clone();
357 assert!(fixes.is_some());
358
359 let fixes = fixes.unwrap();
360 assert_eq!(fixes.len(), 1);
361
362 let fix = fixes.get(0).unwrap();
363 match fix {
364 LintCorrection::Replace(fix) => {
365 assert_eq!(fix.text, "This");
366 assert_eq!(fix.location.start.row, 0);
367 assert_eq!(fix.location.start.column, 2);
368 assert_eq!(fix.location.offset_range.start, AdjustedOffset::from(2));
369 assert_eq!(fix.location.end.row, 0);
370 assert_eq!(fix.location.end.column, 6);
371 assert_eq!(fix.location.offset_range.end, AdjustedOffset::from(6));
372 }
373 _ => panic!("Unexpected fix type"),
374 }
375 }
376
377 #[test]
378 fn test_rule001_uppercase_following_words() {
379 let rule = Rule001HeadingCase::default();
380 let mdx = "# This Should Fail";
381 let parse_result = parse(mdx).unwrap();
382 let context = Context::builder()
383 .parse_result(&parse_result)
384 .build()
385 .unwrap();
386
387 let result = rule.check(
388 parse_result.ast().children().unwrap().first().unwrap(),
389 &context,
390 LintLevel::Error,
391 );
392 assert!(result.is_some());
393
394 let errors = result.unwrap();
395 assert_eq!(errors.len(), 1);
396
397 let fixes = errors.get(0).unwrap().fix.clone();
398 assert!(fixes.is_some());
399
400 let fixes = fixes.unwrap();
401 assert_eq!(fixes.len(), 2);
402
403 let fix_one = fixes.get(0).unwrap();
404 match fix_one {
405 LintCorrection::Replace(fix) => {
406 assert_eq!(fix.text, "should");
407 assert_eq!(fix.location.start.row, 0);
408 assert_eq!(fix.location.start.column, 7);
409 assert_eq!(fix.location.offset_range.start, AdjustedOffset::from(7));
410 assert_eq!(fix.location.end.row, 0);
411 assert_eq!(fix.location.end.column, 13);
412 assert_eq!(fix.location.offset_range.end, AdjustedOffset::from(13));
413 }
414 _ => panic!("Unexpected fix type"),
415 }
416
417 let fix_two = fixes.get(1).unwrap();
418 match fix_two {
419 LintCorrection::Replace(fix) => {
420 assert_eq!(fix.text, "fail");
421 assert_eq!(fix.location.start.row, 0);
422 assert_eq!(fix.location.start.column, 14);
423 assert_eq!(fix.location.offset_range.start, AdjustedOffset::from(14));
424 assert_eq!(fix.location.end.row, 0);
425 assert_eq!(fix.location.end.column, 18);
426 assert_eq!(fix.location.offset_range.end, AdjustedOffset::from(18));
427 }
428 _ => panic!("Unexpected fix type"),
429 }
430 }
431
432 #[test]
433 fn test_rule001_may_uppercase() {
434 let mut rule = Rule001HeadingCase::default();
435 let mut settings = RuleSettings::with_array_of_strings("may_uppercase", vec!["API"]);
436 rule.setup(Some(&mut settings));
437
438 let mdx = "# This is an API heading";
439 let parse_result = parse(mdx).unwrap();
440 let context = Context::builder()
441 .parse_result(&parse_result)
442 .build()
443 .unwrap();
444
445 let result = rule.check(
446 parse_result.ast().children().unwrap().first().unwrap(),
447 &context,
448 LintLevel::Error,
449 );
450 assert!(result.is_none());
451 }
452
453 #[test]
454 fn test_rule001_may_lowercase() {
455 let mut rule = Rule001HeadingCase::default();
456 let mut settings = RuleSettings::with_array_of_strings("may_lowercase", vec!["the"]);
457 rule.setup(Some(&mut settings));
458
459 let mdx = "# the quick brown fox";
460 let parse_result = parse(mdx).unwrap();
461 let context = Context::builder()
462 .parse_result(&parse_result)
463 .build()
464 .unwrap();
465
466 let result = rule.check(
467 parse_result.ast().children().unwrap().first().unwrap(),
468 &context,
469 LintLevel::Error,
470 );
471 assert!(result.is_none());
472 }
473
474 #[test]
475 fn test_rule001_non_heading_node() {
476 let rule = Rule001HeadingCase::default();
477 let mdx = "not a heading";
478 let parse_result = parse(mdx).unwrap();
479 let context = Context::builder()
480 .parse_result(&parse_result)
481 .build()
482 .unwrap();
483
484 let result = rule.check(
485 parse_result.ast().children().unwrap().first().unwrap(),
486 &context,
487 LintLevel::Error,
488 );
489 assert!(result.is_none());
490 }
491
492 #[test]
493 fn test_rule001_may_uppercase_multi_word() {
494 let mut rule = Rule001HeadingCase::default();
495 let mut settings =
496 RuleSettings::with_array_of_strings("may_uppercase", vec!["New York City"]);
497 rule.setup(Some(&mut settings));
498
499 let mdx = "# This is about New York City";
500 let parse_result = parse(mdx).unwrap();
501 let context = Context::builder()
502 .parse_result(&parse_result)
503 .build()
504 .unwrap();
505
506 let result = rule.check(
507 parse_result.ast().children().unwrap().first().unwrap(),
508 &context,
509 LintLevel::Error,
510 );
511 assert!(result.is_none());
512 }
513
514 #[test]
515 fn test_rule001_multiple_exception_matches() {
516 let mut rule = Rule001HeadingCase::default();
517 let mut settings =
518 RuleSettings::with_array_of_strings("may_uppercase", vec!["New York", "New York City"]);
519 rule.setup(Some(&mut settings));
520
521 let mdx = "# This is about New York City";
522 let parse_result = parse(mdx).unwrap();
523 let context = Context::builder()
524 .parse_result(&parse_result)
525 .build()
526 .unwrap();
527
528 let result = rule.check(
529 parse_result.ast().children().unwrap().first().unwrap(),
530 &context,
531 LintLevel::Error,
532 );
533 assert!(result.is_none());
534 }
535
536 #[test]
537 fn test_rule001_may_uppercase_partial_match() {
538 let mut rule = Rule001HeadingCase::default();
539 let mut settings = RuleSettings::with_array_of_strings("may_uppercase", vec!["API"]);
540 rule.setup(Some(&mut settings));
541
542 let mdx = "# This is an API-related topic";
543 let parse_result = parse(mdx).unwrap();
544 let context = Context::builder()
545 .parse_result(&parse_result)
546 .build()
547 .unwrap();
548
549 let result = rule.check(
550 parse_result.ast().children().unwrap().first().unwrap(),
551 &context,
552 LintLevel::Error,
553 );
554 assert!(result.is_none());
555 }
556
557 #[test]
558 fn test_rule001_may_lowercase_regex() {
559 let mut rule = Rule001HeadingCase::default();
560 let mut settings = RuleSettings::with_array_of_strings("may_lowercase", vec!["(the|a|an)"]);
561 rule.setup(Some(&mut settings));
562
563 let mdx = "# the quick brown fox";
564 let parse_result = parse(mdx).unwrap();
565 let context = Context::builder()
566 .parse_result(&parse_result)
567 .build()
568 .unwrap();
569
570 let result = rule.check(
571 parse_result.ast().children().unwrap().first().unwrap(),
572 &context,
573 LintLevel::Error,
574 );
575 assert!(result.is_none());
576 }
577
578 #[test]
579 fn test_rule001_may_uppercase_regex_fails() {
580 let mut rule = Rule001HeadingCase::default();
581 let mut settings = RuleSettings::with_array_of_strings("may_uppercase", vec!["[A-Z]{4,}"]);
582 rule.setup(Some(&mut settings));
583
584 let mdx = "# This is an API call";
585 let parse_result = parse(mdx).unwrap();
586 let context = Context::builder()
587 .parse_result(&parse_result)
588 .build()
589 .unwrap();
590
591 let result = rule.check(
592 parse_result.ast().children().unwrap().first().unwrap(),
593 &context,
594 LintLevel::Error,
595 );
596 assert!(result.is_some());
597
598 let result = result.unwrap();
599 assert_eq!(result.len(), 1);
600
601 let error = result.get(0).unwrap();
602 assert_eq!(error.fix.as_ref().unwrap().len(), 1);
603
604 let fixes = error.fix.clone().unwrap();
605 let fix = fixes.get(0).unwrap();
606 match fix {
607 LintCorrection::Replace(fix) => {
608 assert_eq!(fix.text, "api");
609 assert_eq!(fix.location.start.row, 0);
610 assert_eq!(fix.location.start.column, 13);
611 assert_eq!(fix.location.offset_range.start, AdjustedOffset::from(13));
612 assert_eq!(fix.location.end.row, 0);
613 assert_eq!(fix.location.end.column, 16);
614 assert_eq!(fix.location.offset_range.end, AdjustedOffset::from(16));
615 }
616 _ => panic!("Unexpected fix type"),
617 }
618 }
619
620 #[test]
621 fn test_rule001_multi_word_exception_at_start() {
622 let mut rule = Rule001HeadingCase::default();
623 let mut settings =
624 RuleSettings::with_array_of_strings("may_uppercase", vec!["Content Delivery Network"]);
625 rule.setup(Some(&mut settings));
626
627 let mdx = "# Content Delivery Network latency";
628 let parse_result = parse(mdx).unwrap();
629 let context = Context::builder()
630 .parse_result(&parse_result)
631 .build()
632 .unwrap();
633
634 let result = rule.check(
635 parse_result.ast().children().unwrap().first().unwrap(),
636 &context,
637 LintLevel::Error,
638 );
639 assert!(result.is_none());
640 }
641
642 #[test]
643 fn test_rule001_multi_word_exception_in_middle() {
644 let mut rule = Rule001HeadingCase::default();
645 let mut settings = RuleSettings::with_array_of_strings("may_uppercase", vec!["Magic Link"]);
646 rule.setup(Some(&mut settings));
647
648 let markdown = "### Enabling Magic Link signins";
649 let parse_result = parse(markdown).unwrap();
650 let context = Context::builder()
651 .parse_result(&parse_result)
652 .build()
653 .unwrap();
654
655 let result = rule.check(
656 parse_result.ast().children().unwrap().first().unwrap(),
657 &context,
658 LintLevel::Error,
659 );
660
661 assert!(result.is_none());
662 }
663
664 #[test]
665 fn test_rule001_brackets_around_exception() {
666 let mut rule = Rule001HeadingCase::default();
667 let mut settings =
668 RuleSettings::with_array_of_strings("may_uppercase", vec!["Edge Functions"]);
669 rule.setup(Some(&mut settings));
670
671 let mdx = "# Deno (Edge Functions)";
672 let parse_result = parse(mdx).unwrap();
673 let context = Context::builder()
674 .parse_result(&parse_result)
675 .build()
676 .unwrap();
677
678 let result = rule.check(
679 parse_result.ast().children().unwrap().first().unwrap(),
680 &context,
681 LintLevel::Error,
682 );
683 assert!(result.is_none());
684 }
685
686 #[test]
687 fn test_rule001_complex_heading() {
688 let mut rule = Rule001HeadingCase::default();
689 let mut settings =
690 RuleSettings::with_array_of_strings("may_uppercase", vec!["API", "OAuth"]);
691 rule.setup(Some(&mut settings));
692
693 let mdx = "# The basics of API authentication in OAuth";
694 let parse_result = parse(mdx).unwrap();
695 let context = Context::builder()
696 .parse_result(&parse_result)
697 .build()
698 .unwrap();
699
700 let result = rule.check(
701 parse_result.ast().children().unwrap().first().unwrap(),
702 &context,
703 LintLevel::Error,
704 );
705 assert!(result.is_none());
706 }
707
708 #[test]
709 fn test_rule001_can_capitalize_after_colon() {
710 let mut rule = Rule001HeadingCase::default();
711 rule.setup(None);
712
713 let mdx = "# Bonus: Profile photos";
714 let parse_result = parse(mdx).unwrap();
715 let context = Context::builder()
716 .parse_result(&parse_result)
717 .build()
718 .unwrap();
719
720 let result = rule.check(
721 parse_result.ast().children().unwrap().first().unwrap(),
722 &context,
723 LintLevel::Error,
724 );
725 assert!(result.is_none());
726 }
727
728 #[test]
729 fn test_rule001_can_capitalize_after_colon_with_number() {
730 let mut rule = Rule001HeadingCase::default();
731 rule.setup(None);
732
733 let mdx = "# Step 1: Do a thing";
734 let parse_result = parse(mdx).unwrap();
735 let context = Context::builder()
736 .parse_result(&parse_result)
737 .build()
738 .unwrap();
739
740 let result = rule.check(
741 parse_result.ast().children().unwrap().first().unwrap(),
742 &context,
743 LintLevel::Error,
744 );
745 assert!(result.is_none());
746 }
747
748 #[test]
749 fn test_rule001_can_capitalize_after_sentence_break() {
750 let mut rule = Rule001HeadingCase::default();
751 rule.setup(None);
752
753 let mdx = "# 1. Do a thing";
754 let parse_result = parse(mdx).unwrap();
755 let context = Context::builder()
756 .parse_result(&parse_result)
757 .build()
758 .unwrap();
759
760 let result = rule.check(
761 parse_result.ast().children().unwrap().first().unwrap(),
762 &context,
763 LintLevel::Error,
764 );
765 assert!(result.is_none());
766 }
767
768 #[test]
769 fn test_rule001_no_flag_inline_code() {
770 let mut rule = Rule001HeadingCase::default();
771 rule.setup(None);
772
773 let markdown = "# `inline_code` (in a heading) can have `ArbitraryCase`";
774 let parse_result = parse(markdown).unwrap();
775 let context = Context::builder()
776 .parse_result(&parse_result)
777 .build()
778 .unwrap();
779
780 let result = rule.check(
781 parse_result.ast().children().unwrap().first().unwrap(),
782 &context,
783 LintLevel::Error,
784 );
785 assert!(result.is_none());
786 }
787
788 #[test]
789 fn test_rule001_heading_starts_with_number() {
790 let mut rule = Rule001HeadingCase::default();
791 rule.setup(None);
792
793 let markdown = "# 384 dimensions for vector";
794 let parse_result = parse(markdown).unwrap();
795 let context = Context::builder()
796 .parse_result(&parse_result)
797 .build()
798 .unwrap();
799
800 let result = rule.check(
801 parse_result.ast().children().unwrap().first().unwrap(),
802 &context,
803 LintLevel::Error,
804 );
805 assert!(result.is_none());
806 }
807
808 #[test]
809 fn test_rule001_heading_starts_with_may_uppercase_exception() {
810 let mut rule = Rule001HeadingCase::default();
811 let mut settings = RuleSettings::with_array_of_strings("may_uppercase", vec!["API"]);
812 rule.setup(Some(&mut settings));
813
814 let markdown = "### API Error codes";
815 let parse_result = parse(markdown).unwrap();
816 let context = Context::builder()
817 .parse_result(&parse_result)
818 .build()
819 .unwrap();
820
821 let result = rule
822 .check(
823 parse_result.ast().children().unwrap().first().unwrap(),
824 &context,
825 LintLevel::Error,
826 )
827 .unwrap();
828
829 let fixes = result.get(0).unwrap().fix.as_ref().unwrap();
830 let fix = fixes.get(0).unwrap();
831 match fix {
832 LintCorrection::Replace(fix) => {
833 assert_eq!(fix.location.start.column, 8);
834 }
835 _ => panic!("Unexpected fix type"),
836 }
837 }
838
839 #[test]
840 fn test_rule001_heading_contains_link() {
841 let mut rule = Rule001HeadingCase::default();
842 rule.setup(None);
843
844 let markdown = "## Filtering with [regular expressions](https://en.wikipedia.org/wiki/Regular_expression)";
845 let parse_result = parse(markdown).unwrap();
846 let context = Context::builder()
847 .parse_result(&parse_result)
848 .build()
849 .unwrap();
850
851 let result = rule.check(
852 parse_result.ast().children().unwrap().first().unwrap(),
853 &context,
854 LintLevel::Error,
855 );
856 assert!(result.is_none());
857 }
858}