1use crate::utils::range_utils::calculate_match_range;
2use crate::utils::regex_cache::{BOLD_ASTERISK_REGEX, BOLD_UNDERSCORE_REGEX};
3
4use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, Severity};
5use crate::rules::strong_style::StrongStyle;
6use crate::utils::regex_cache::get_cached_regex;
7use crate::utils::skip_context::is_in_mkdocs_markup;
8
9const REF_DEF_REGEX_STR: &str = r#"(?m)^[ ]{0,3}\[([^\]]+)\]:\s*([^\s]+)(?:\s+(?:"([^"]*)"|'([^']*)'))?$"#;
11
12mod md050_config;
13use md050_config::MD050Config;
14
15#[derive(Debug, Default, Clone)]
21pub struct MD050StrongStyle {
22 config: MD050Config,
23}
24
25impl MD050StrongStyle {
26 pub fn new(style: StrongStyle) -> Self {
27 Self {
28 config: MD050Config { style },
29 }
30 }
31
32 pub fn from_config_struct(config: MD050Config) -> Self {
33 Self { config }
34 }
35
36 fn is_in_link(&self, ctx: &crate::lint_context::LintContext, byte_pos: usize) -> bool {
38 for link in &ctx.links {
40 if link.byte_offset <= byte_pos && byte_pos < link.byte_end {
41 return true;
42 }
43 }
44
45 for image in &ctx.images {
47 if image.byte_offset <= byte_pos && byte_pos < image.byte_end {
48 return true;
49 }
50 }
51
52 if let Ok(re) = get_cached_regex(REF_DEF_REGEX_STR) {
54 for m in re.find_iter(ctx.content) {
55 if m.start() <= byte_pos && byte_pos < m.end() {
56 return true;
57 }
58 }
59 }
60
61 false
62 }
63
64 fn is_in_html_tag(&self, ctx: &crate::lint_context::LintContext, byte_pos: usize) -> bool {
66 for html_tag in ctx.html_tags().iter() {
68 if html_tag.byte_offset <= byte_pos && byte_pos < html_tag.byte_end {
71 return true;
72 }
73 }
74 false
75 }
76
77 fn is_in_html_code_content(&self, ctx: &crate::lint_context::LintContext, byte_pos: usize) -> bool {
80 let html_tags = ctx.html_tags();
81 let mut open_code_pos: Option<usize> = None;
82
83 for tag in html_tags.iter() {
84 if tag.byte_offset > byte_pos {
86 return open_code_pos.is_some();
87 }
88
89 if tag.tag_name == "code" {
90 if tag.is_self_closing {
91 continue;
93 } else if !tag.is_closing {
94 open_code_pos = Some(tag.byte_end);
96 } else if tag.is_closing && open_code_pos.is_some() {
97 if let Some(open_pos) = open_code_pos
99 && byte_pos >= open_pos
100 && byte_pos < tag.byte_offset
101 {
102 return true;
104 }
105 open_code_pos = None;
106 }
107 }
108 }
109
110 open_code_pos.is_some() && byte_pos >= open_code_pos.unwrap()
112 }
113
114 fn detect_style(&self, ctx: &crate::lint_context::LintContext) -> Option<StrongStyle> {
115 let content = ctx.content;
116 let lines: Vec<&str> = content.lines().collect();
117
118 let mut asterisk_count = 0;
120 for m in BOLD_ASTERISK_REGEX.find_iter(content) {
121 let (line_num, col) = ctx.offset_to_line_col(m.start());
123 let in_front_matter = ctx
124 .line_info(line_num)
125 .map(|info| info.in_front_matter)
126 .unwrap_or(false);
127
128 let in_mkdocs_markup = lines
130 .get(line_num.saturating_sub(1))
131 .is_some_and(|line| is_in_mkdocs_markup(line, col.saturating_sub(1), ctx.flavor));
132
133 if !in_front_matter
134 && !ctx.is_in_code_block_or_span(m.start())
135 && !self.is_in_link(ctx, m.start())
136 && !self.is_in_html_tag(ctx, m.start())
137 && !self.is_in_html_code_content(ctx, m.start())
138 && !in_mkdocs_markup
139 {
140 asterisk_count += 1;
141 }
142 }
143
144 let mut underscore_count = 0;
145 for m in BOLD_UNDERSCORE_REGEX.find_iter(content) {
146 let (line_num, col) = ctx.offset_to_line_col(m.start());
148 let in_front_matter = ctx
149 .line_info(line_num)
150 .map(|info| info.in_front_matter)
151 .unwrap_or(false);
152
153 let in_mkdocs_markup = lines
155 .get(line_num.saturating_sub(1))
156 .is_some_and(|line| is_in_mkdocs_markup(line, col.saturating_sub(1), ctx.flavor));
157
158 if !in_front_matter
159 && !ctx.is_in_code_block_or_span(m.start())
160 && !self.is_in_link(ctx, m.start())
161 && !self.is_in_html_tag(ctx, m.start())
162 && !self.is_in_html_code_content(ctx, m.start())
163 && !in_mkdocs_markup
164 {
165 underscore_count += 1;
166 }
167 }
168
169 match (asterisk_count, underscore_count) {
170 (0, 0) => None,
171 (_, 0) => Some(StrongStyle::Asterisk),
172 (0, _) => Some(StrongStyle::Underscore),
173 (a, u) => {
174 if a >= u {
177 Some(StrongStyle::Asterisk)
178 } else {
179 Some(StrongStyle::Underscore)
180 }
181 }
182 }
183 }
184
185 fn is_escaped(&self, text: &str, pos: usize) -> bool {
186 if pos == 0 {
187 return false;
188 }
189
190 let mut backslash_count = 0;
191 let mut i = pos;
192 let bytes = text.as_bytes();
193 while i > 0 {
194 i -= 1;
195 if i < bytes.len() && bytes[i] != b'\\' {
197 break;
198 }
199 backslash_count += 1;
200 }
201 backslash_count % 2 == 1
202 }
203}
204
205impl Rule for MD050StrongStyle {
206 fn name(&self) -> &'static str {
207 "MD050"
208 }
209
210 fn description(&self) -> &'static str {
211 "Strong emphasis style should be consistent"
212 }
213
214 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
215 let content = ctx.content;
216 let line_index = &ctx.line_index;
217
218 let mut warnings = Vec::new();
219
220 let target_style = match self.config.style {
221 StrongStyle::Consistent => self.detect_style(ctx).unwrap_or(StrongStyle::Asterisk),
222 _ => self.config.style,
223 };
224
225 let strong_regex = match target_style {
226 StrongStyle::Asterisk => &*BOLD_UNDERSCORE_REGEX,
227 StrongStyle::Underscore => &*BOLD_ASTERISK_REGEX,
228 StrongStyle::Consistent => {
229 &*BOLD_UNDERSCORE_REGEX
232 }
233 };
234
235 for (line_num, line) in content.lines().enumerate() {
236 if let Some(line_info) = ctx.line_info(line_num + 1)
238 && line_info.in_front_matter
239 {
240 continue;
241 }
242
243 let byte_pos = line_index.get_line_start_byte(line_num + 1).unwrap_or(0);
244
245 for m in strong_regex.find_iter(line) {
246 let match_byte_pos = byte_pos + m.start();
248
249 if ctx.is_in_code_block_or_span(match_byte_pos)
251 || self.is_in_link(ctx, match_byte_pos)
252 || self.is_in_html_code_content(ctx, match_byte_pos)
253 || is_in_mkdocs_markup(line, m.start(), ctx.flavor)
254 {
255 continue;
256 }
257
258 let mut inside_html_tag = false;
261 for tag in ctx.html_tags().iter() {
262 if tag.byte_offset < match_byte_pos && match_byte_pos < tag.byte_end - 1 {
264 inside_html_tag = true;
265 break;
266 }
267 }
268 if inside_html_tag {
269 continue;
270 }
271
272 if !self.is_escaped(line, m.start()) {
273 let text = &line[m.start() + 2..m.end() - 2];
274
275 let message = match target_style {
282 StrongStyle::Asterisk => "Strong emphasis should use ** instead of __",
283 StrongStyle::Underscore => "Strong emphasis should use __ instead of **",
284 StrongStyle::Consistent => "Strong emphasis should use ** instead of __",
285 };
286
287 let (start_line, start_col, end_line, end_col) =
289 calculate_match_range(line_num + 1, line, m.start(), m.len());
290
291 warnings.push(LintWarning {
292 rule_name: Some(self.name().to_string()),
293 line: start_line,
294 column: start_col,
295 end_line,
296 end_column: end_col,
297 message: message.to_string(),
298 severity: Severity::Warning,
299 fix: Some(Fix {
300 range: line_index.line_col_to_byte_range(line_num + 1, m.start() + 1),
301 replacement: match target_style {
302 StrongStyle::Asterisk => format!("**{text}**"),
303 StrongStyle::Underscore => format!("__{text}__"),
304 StrongStyle::Consistent => format!("**{text}**"),
305 },
306 }),
307 });
308 }
309 }
310 }
311
312 Ok(warnings)
313 }
314
315 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
316 let content = ctx.content;
317
318 let target_style = match self.config.style {
319 StrongStyle::Consistent => self.detect_style(ctx).unwrap_or(StrongStyle::Asterisk),
320 _ => self.config.style,
321 };
322
323 let strong_regex = match target_style {
324 StrongStyle::Asterisk => &*BOLD_UNDERSCORE_REGEX,
325 StrongStyle::Underscore => &*BOLD_ASTERISK_REGEX,
326 StrongStyle::Consistent => {
327 &*BOLD_UNDERSCORE_REGEX
330 }
331 };
332
333 let lines: Vec<&str> = content.lines().collect();
335
336 let matches: Vec<(usize, usize)> = strong_regex
337 .find_iter(content)
338 .filter(|m| {
339 let (line_num, col) = ctx.offset_to_line_col(m.start());
341 if let Some(line_info) = ctx.line_info(line_num)
342 && line_info.in_front_matter
343 {
344 return false;
345 }
346 let in_mkdocs_markup = lines
348 .get(line_num.saturating_sub(1))
349 .is_some_and(|line| is_in_mkdocs_markup(line, col.saturating_sub(1), ctx.flavor));
350 !ctx.is_in_code_block_or_span(m.start())
351 && !self.is_in_link(ctx, m.start())
352 && !self.is_in_html_tag(ctx, m.start())
353 && !self.is_in_html_code_content(ctx, m.start())
354 && !in_mkdocs_markup
355 })
356 .filter(|m| !self.is_escaped(content, m.start()))
357 .map(|m| (m.start(), m.end()))
358 .collect();
359
360 let mut result = content.to_string();
363 for (start, end) in matches.into_iter().rev() {
364 let text = &result[start + 2..end - 2];
365 let replacement = match target_style {
366 StrongStyle::Asterisk => format!("**{text}**"),
367 StrongStyle::Underscore => format!("__{text}__"),
368 StrongStyle::Consistent => {
369 format!("**{text}**")
372 }
373 };
374 result.replace_range(start..end, &replacement);
375 }
376
377 Ok(result)
378 }
379
380 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
382 ctx.content.is_empty() || !ctx.likely_has_emphasis()
384 }
385
386 fn as_any(&self) -> &dyn std::any::Any {
387 self
388 }
389
390 fn default_config_section(&self) -> Option<(String, toml::Value)> {
391 let json_value = serde_json::to_value(&self.config).ok()?;
392 Some((
393 self.name().to_string(),
394 crate::rule_config_serde::json_to_toml_value(&json_value)?,
395 ))
396 }
397
398 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
399 where
400 Self: Sized,
401 {
402 let rule_config = crate::rule_config_serde::load_rule_config::<MD050Config>(config);
403 Box::new(Self::from_config_struct(rule_config))
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410 use crate::lint_context::LintContext;
411
412 #[test]
413 fn test_asterisk_style_with_asterisks() {
414 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
415 let content = "This is **strong text** here.";
416 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
417 let result = rule.check(&ctx).unwrap();
418
419 assert_eq!(result.len(), 0);
420 }
421
422 #[test]
423 fn test_asterisk_style_with_underscores() {
424 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
425 let content = "This is __strong text__ here.";
426 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
427 let result = rule.check(&ctx).unwrap();
428
429 assert_eq!(result.len(), 1);
430 assert!(
431 result[0]
432 .message
433 .contains("Strong emphasis should use ** instead of __")
434 );
435 assert_eq!(result[0].line, 1);
436 assert_eq!(result[0].column, 9);
437 }
438
439 #[test]
440 fn test_underscore_style_with_underscores() {
441 let rule = MD050StrongStyle::new(StrongStyle::Underscore);
442 let content = "This is __strong text__ here.";
443 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
444 let result = rule.check(&ctx).unwrap();
445
446 assert_eq!(result.len(), 0);
447 }
448
449 #[test]
450 fn test_underscore_style_with_asterisks() {
451 let rule = MD050StrongStyle::new(StrongStyle::Underscore);
452 let content = "This is **strong text** here.";
453 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
454 let result = rule.check(&ctx).unwrap();
455
456 assert_eq!(result.len(), 1);
457 assert!(
458 result[0]
459 .message
460 .contains("Strong emphasis should use __ instead of **")
461 );
462 }
463
464 #[test]
465 fn test_consistent_style_first_asterisk() {
466 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
467 let content = "First **strong** then __also strong__.";
468 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
469 let result = rule.check(&ctx).unwrap();
470
471 assert_eq!(result.len(), 1);
473 assert!(
474 result[0]
475 .message
476 .contains("Strong emphasis should use ** instead of __")
477 );
478 }
479
480 #[test]
481 fn test_consistent_style_tie_prefers_asterisk() {
482 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
483 let content = "First __strong__ then **also strong**.";
484 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
485 let result = rule.check(&ctx).unwrap();
486
487 assert_eq!(result.len(), 1);
490 assert!(
491 result[0]
492 .message
493 .contains("Strong emphasis should use ** instead of __")
494 );
495 }
496
497 #[test]
498 fn test_detect_style_asterisk() {
499 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
500 let ctx = LintContext::new(
501 "This has **strong** text.",
502 crate::config::MarkdownFlavor::Standard,
503 None,
504 );
505 let style = rule.detect_style(&ctx);
506
507 assert_eq!(style, Some(StrongStyle::Asterisk));
508 }
509
510 #[test]
511 fn test_detect_style_underscore() {
512 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
513 let ctx = LintContext::new(
514 "This has __strong__ text.",
515 crate::config::MarkdownFlavor::Standard,
516 None,
517 );
518 let style = rule.detect_style(&ctx);
519
520 assert_eq!(style, Some(StrongStyle::Underscore));
521 }
522
523 #[test]
524 fn test_detect_style_none() {
525 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
526 let ctx = LintContext::new("No strong text here.", crate::config::MarkdownFlavor::Standard, None);
527 let style = rule.detect_style(&ctx);
528
529 assert_eq!(style, None);
530 }
531
532 #[test]
533 fn test_strong_in_code_block() {
534 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
535 let content = "```\n__strong__ in code\n```\n__strong__ outside";
536 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
537 let result = rule.check(&ctx).unwrap();
538
539 assert_eq!(result.len(), 1);
541 assert_eq!(result[0].line, 4);
542 }
543
544 #[test]
545 fn test_strong_in_inline_code() {
546 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
547 let content = "Text with `__strong__` in code and __strong__ outside.";
548 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
549 let result = rule.check(&ctx).unwrap();
550
551 assert_eq!(result.len(), 1);
553 }
554
555 #[test]
556 fn test_escaped_strong() {
557 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
558 let content = "This is \\__not strong\\__ but __this is__.";
559 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
560 let result = rule.check(&ctx).unwrap();
561
562 assert_eq!(result.len(), 1);
564 assert_eq!(result[0].line, 1);
565 assert_eq!(result[0].column, 30);
566 }
567
568 #[test]
569 fn test_fix_asterisks_to_underscores() {
570 let rule = MD050StrongStyle::new(StrongStyle::Underscore);
571 let content = "This is **strong** text.";
572 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
573 let fixed = rule.fix(&ctx).unwrap();
574
575 assert_eq!(fixed, "This is __strong__ text.");
576 }
577
578 #[test]
579 fn test_fix_underscores_to_asterisks() {
580 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
581 let content = "This is __strong__ text.";
582 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
583 let fixed = rule.fix(&ctx).unwrap();
584
585 assert_eq!(fixed, "This is **strong** text.");
586 }
587
588 #[test]
589 fn test_fix_multiple_strong() {
590 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
591 let content = "First __strong__ and second __also strong__.";
592 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
593 let fixed = rule.fix(&ctx).unwrap();
594
595 assert_eq!(fixed, "First **strong** and second **also strong**.");
596 }
597
598 #[test]
599 fn test_fix_preserves_code_blocks() {
600 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
601 let content = "```\n__strong__ in code\n```\n__strong__ outside";
602 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
603 let fixed = rule.fix(&ctx).unwrap();
604
605 assert_eq!(fixed, "```\n__strong__ in code\n```\n**strong** outside");
606 }
607
608 #[test]
609 fn test_multiline_content() {
610 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
611 let content = "Line 1 with __strong__\nLine 2 with __another__\nLine 3 normal";
612 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
613 let result = rule.check(&ctx).unwrap();
614
615 assert_eq!(result.len(), 2);
616 assert_eq!(result[0].line, 1);
617 assert_eq!(result[1].line, 2);
618 }
619
620 #[test]
621 fn test_nested_emphasis() {
622 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
623 let content = "This has __strong with *emphasis* inside__.";
624 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
625 let result = rule.check(&ctx).unwrap();
626
627 assert_eq!(result.len(), 1);
628 }
629
630 #[test]
631 fn test_empty_content() {
632 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
633 let content = "";
634 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
635 let result = rule.check(&ctx).unwrap();
636
637 assert_eq!(result.len(), 0);
638 }
639
640 #[test]
641 fn test_default_config() {
642 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
643 let (name, _config) = rule.default_config_section().unwrap();
644 assert_eq!(name, "MD050");
645 }
646
647 #[test]
648 fn test_strong_in_links_not_flagged() {
649 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
650 let content = r#"Instead of assigning to `self.value`, we're relying on the [`__dict__`][__dict__] in our object to hold that value instead.
651
652Hint:
653
654- [An article on something](https://blog.yuo.be/2018/08/16/__init_subclass__-a-simpler-way-to-implement-class-registries-in-python/ "Some details on using `__init_subclass__`")
655
656
657[__dict__]: https://www.pythonmorsels.com/where-are-attributes-stored/"#;
658 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
659 let result = rule.check(&ctx).unwrap();
660
661 assert_eq!(result.len(), 0);
663 }
664
665 #[test]
666 fn test_strong_in_links_vs_outside_links() {
667 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
668 let content = r#"We're doing this because generator functions return a generator object which [is an iterator][generators are iterators] and **we need `__iter__` to return an [iterator][]**.
669
670Instead of assigning to `self.value`, we're relying on the [`__dict__`][__dict__] in our object to hold that value instead.
671
672This is __real strong text__ that should be flagged.
673
674[__dict__]: https://www.pythonmorsels.com/where-are-attributes-stored/"#;
675 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
676 let result = rule.check(&ctx).unwrap();
677
678 assert_eq!(result.len(), 1);
680 assert!(
681 result[0]
682 .message
683 .contains("Strong emphasis should use ** instead of __")
684 );
685 assert!(result[0].line > 4); }
688
689 #[test]
690 fn test_front_matter_not_flagged() {
691 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
692 let content = "---\ntitle: What's __init__.py?\nother: __value__\n---\n\nThis __should be flagged__.";
693 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
694 let result = rule.check(&ctx).unwrap();
695
696 assert_eq!(result.len(), 1);
698 assert_eq!(result[0].line, 6);
699 assert!(
700 result[0]
701 .message
702 .contains("Strong emphasis should use ** instead of __")
703 );
704 }
705
706 #[test]
707 fn test_html_tags_not_flagged() {
708 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
709 let content = r#"# Test
710
711This has HTML with underscores:
712
713<iframe src="https://example.com/__init__/__repr__"> </iframe>
714
715This __should be flagged__ as inconsistent."#;
716 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
717 let result = rule.check(&ctx).unwrap();
718
719 assert_eq!(result.len(), 1);
721 assert_eq!(result[0].line, 7);
722 assert!(
723 result[0]
724 .message
725 .contains("Strong emphasis should use ** instead of __")
726 );
727 }
728
729 #[test]
730 fn test_mkdocs_keys_notation_not_flagged() {
731 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
733 let content = "Press ++ctrl+alt+del++ to restart.";
734 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
735 let result = rule.check(&ctx).unwrap();
736
737 assert!(
739 result.is_empty(),
740 "Keys notation should not be flagged as strong emphasis. Got: {result:?}"
741 );
742 }
743
744 #[test]
745 fn test_mkdocs_caret_notation_not_flagged() {
746 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
748 let content = "This is ^^inserted^^ text.";
749 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
750 let result = rule.check(&ctx).unwrap();
751
752 assert!(
753 result.is_empty(),
754 "Insert notation should not be flagged as strong emphasis. Got: {result:?}"
755 );
756 }
757
758 #[test]
759 fn test_mkdocs_mark_notation_not_flagged() {
760 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
762 let content = "This is ==highlighted== text.";
763 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
764 let result = rule.check(&ctx).unwrap();
765
766 assert!(
767 result.is_empty(),
768 "Mark notation should not be flagged as strong emphasis. Got: {result:?}"
769 );
770 }
771
772 #[test]
773 fn test_mkdocs_mixed_content_with_real_strong() {
774 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
776 let content = "Press ++ctrl++ and __underscore strong__ here.";
777 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
778 let result = rule.check(&ctx).unwrap();
779
780 assert_eq!(result.len(), 1, "Expected 1 warning, got: {result:?}");
782 assert!(
783 result[0]
784 .message
785 .contains("Strong emphasis should use ** instead of __")
786 );
787 }
788
789 #[test]
790 fn test_mkdocs_icon_shortcode_not_flagged() {
791 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
793 let content = "Click :material-check: and __this should be flagged__.";
794 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
795 let result = rule.check(&ctx).unwrap();
796
797 assert_eq!(result.len(), 1);
799 assert!(
800 result[0]
801 .message
802 .contains("Strong emphasis should use ** instead of __")
803 );
804 }
805}