supa_mdx_lint/rules/
rule001_heading_case.rs

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/// Headings should be in sentence case.
23///
24/// ## Examples
25///
26/// ### Valid
27///
28/// ```markdown
29/// # This is sentence case
30/// ```
31///
32/// ### Invalid
33///
34/// ```markdown
35/// # This is Not Sentence Case
36/// ```
37///
38/// ## Exceptions
39///
40/// Exceptions are configured via the `may_uppercase` and `may_lowercase` arrays.
41/// - `may_uppercase`: Words that may be capitalized even if they are not the first word in the heading.
42/// - `may_lowercase`: Words that may be lowercased even if they are the first word in the heading.
43///
44/// See an  [example from the Supabase repo](https://github.com/supabase/supabase/blob/master/supa-mdx-lint/Rule001HeadingCase.toml).
45#[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(&regex_settings))
75            {
76                self.may_uppercase = vec;
77            }
78            if let Some(vec) = settings.get_array_of_regexes("may_lowercase", Some(&regex_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}