1use crate::utils::range_utils::calculate_match_range;
2
3use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
4use crate::rules::strong_style::StrongStyle;
5use crate::utils::code_block_utils::StrongSpanDetail;
6use crate::utils::skip_context::{compute_html_code_ranges, should_skip_emphasis_span};
7
8fn span_style(span: &StrongSpanDetail) -> StrongStyle {
10 if span.is_asterisk {
11 StrongStyle::Asterisk
12 } else {
13 StrongStyle::Underscore
14 }
15}
16
17mod md050_config;
18use md050_config::MD050Config;
19
20#[derive(Debug, Default, Clone)]
26pub struct MD050StrongStyle {
27 config: MD050Config,
28}
29
30impl MD050StrongStyle {
31 pub fn new(style: StrongStyle) -> Self {
32 Self {
33 config: MD050Config { style },
34 }
35 }
36
37 pub fn from_config_struct(config: MD050Config) -> Self {
38 Self { config }
39 }
40
41 #[cfg(test)]
42 fn detect_style(&self, ctx: &crate::lint_context::LintContext) -> Option<StrongStyle> {
43 let html_tags = ctx.html_tags();
44 let html_code_ranges = compute_html_code_ranges(&html_tags);
45 self.detect_style_from_spans(ctx, &html_tags, &html_code_ranges, &ctx.strong_spans)
46 }
47
48 fn detect_style_from_spans(
49 &self,
50 ctx: &crate::lint_context::LintContext,
51 html_tags: &[crate::lint_context::HtmlTag],
52 html_code_ranges: &[(usize, usize)],
53 spans: &[StrongSpanDetail],
54 ) -> Option<StrongStyle> {
55 let mut asterisk_count = 0;
56 let mut underscore_count = 0;
57
58 for span in spans {
59 if should_skip_emphasis_span(ctx, html_tags, html_code_ranges, span.start) {
60 continue;
61 }
62
63 match span_style(span) {
64 StrongStyle::Asterisk => asterisk_count += 1,
65 StrongStyle::Underscore => underscore_count += 1,
66 StrongStyle::Consistent => {}
67 }
68 }
69
70 match (asterisk_count, underscore_count) {
71 (0, 0) => None,
72 (_, 0) => Some(StrongStyle::Asterisk),
73 (0, _) => Some(StrongStyle::Underscore),
74 (a, u) => {
76 if a >= u {
77 Some(StrongStyle::Asterisk)
78 } else {
79 Some(StrongStyle::Underscore)
80 }
81 }
82 }
83 }
84}
85
86impl Rule for MD050StrongStyle {
87 fn name(&self) -> &'static str {
88 "MD050"
89 }
90
91 fn description(&self) -> &'static str {
92 "Strong emphasis style should be consistent"
93 }
94
95 fn category(&self) -> RuleCategory {
96 RuleCategory::Emphasis
97 }
98
99 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
100 let content = ctx.content;
101 let line_index = &ctx.line_index;
102 let lines = ctx.raw_lines();
103
104 let mut warnings = Vec::new();
105
106 let spans = &ctx.strong_spans;
107 let html_tags = ctx.html_tags();
108 let html_code_ranges = compute_html_code_ranges(&html_tags);
109
110 let target_style = match self.config.style {
111 StrongStyle::Consistent => self
112 .detect_style_from_spans(ctx, &html_tags, &html_code_ranges, spans)
113 .unwrap_or(StrongStyle::Asterisk),
114 _ => self.config.style,
115 };
116
117 for span in spans {
118 if span_style(span) == target_style {
120 continue;
121 }
122
123 if span.end - span.start < 4 {
125 continue;
126 }
127
128 if should_skip_emphasis_span(ctx, &html_tags, &html_code_ranges, span.start) {
130 continue;
131 }
132
133 let (line_num, _col) = ctx.offset_to_line_col(span.start);
134 let line_start = line_index.get_line_start_byte(line_num).unwrap_or(0);
135 let line_content = lines.get(line_num - 1).unwrap_or(&"");
136 let match_start_in_line = span.start - line_start;
137 let match_len = span.end - span.start;
138
139 let inner_text = &content[span.start + 2..span.end - 2];
140
141 let message = match target_style {
148 StrongStyle::Asterisk => "Strong emphasis should use ** instead of __",
149 StrongStyle::Underscore => "Strong emphasis should use __ instead of **",
150 StrongStyle::Consistent => "Strong emphasis should use ** instead of __",
151 };
152
153 let (start_line, start_col, end_line, end_col) =
154 calculate_match_range(line_num, line_content, match_start_in_line, match_len);
155
156 warnings.push(LintWarning {
157 rule_name: Some(self.name().to_string()),
158 line: start_line,
159 column: start_col,
160 end_line,
161 end_column: end_col,
162 message: message.to_string(),
163 severity: Severity::Warning,
164 fix: Some(Fix::new(
165 span.start..span.end,
166 match target_style {
167 StrongStyle::Asterisk => format!("**{inner_text}**"),
168 StrongStyle::Underscore => format!("__{inner_text}__"),
169 StrongStyle::Consistent => format!("**{inner_text}**"),
170 },
171 )),
172 });
173 }
174
175 Ok(warnings)
176 }
177
178 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
179 if self.should_skip(ctx) {
180 return Ok(ctx.content.to_string());
181 }
182 let warnings = self.check(ctx)?;
183 if warnings.is_empty() {
184 return Ok(ctx.content.to_string());
185 }
186 let warnings =
187 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
188 crate::utils::fix_utils::apply_warning_fixes(ctx.content, &warnings)
189 .map_err(crate::rule::LintError::InvalidInput)
190 }
191
192 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
194 ctx.content.is_empty() || !ctx.likely_has_emphasis()
196 }
197
198 fn as_any(&self) -> &dyn std::any::Any {
199 self
200 }
201
202 fn default_config_section(&self) -> Option<(String, toml::Value)> {
203 let json_value = serde_json::to_value(&self.config).ok()?;
204 Some((
205 self.name().to_string(),
206 crate::rule_config_serde::json_to_toml_value(&json_value)?,
207 ))
208 }
209
210 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
211 where
212 Self: Sized,
213 {
214 let rule_config = crate::rule_config_serde::load_rule_config::<MD050Config>(config);
215 Box::new(Self::from_config_struct(rule_config))
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use crate::lint_context::LintContext;
223
224 #[test]
225 fn test_asterisk_style_with_asterisks() {
226 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
227 let content = "This is **strong text** here.";
228 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
229 let result = rule.check(&ctx).unwrap();
230
231 assert_eq!(result.len(), 0);
232 }
233
234 #[test]
235 fn test_asterisk_style_with_underscores() {
236 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
237 let content = "This is __strong text__ here.";
238 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
239 let result = rule.check(&ctx).unwrap();
240
241 assert_eq!(result.len(), 1);
242 assert!(
243 result[0]
244 .message
245 .contains("Strong emphasis should use ** instead of __")
246 );
247 assert_eq!(result[0].line, 1);
248 assert_eq!(result[0].column, 9);
249 }
250
251 #[test]
252 fn test_underscore_style_with_underscores() {
253 let rule = MD050StrongStyle::new(StrongStyle::Underscore);
254 let content = "This is __strong text__ here.";
255 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
256 let result = rule.check(&ctx).unwrap();
257
258 assert_eq!(result.len(), 0);
259 }
260
261 #[test]
262 fn test_underscore_style_with_asterisks() {
263 let rule = MD050StrongStyle::new(StrongStyle::Underscore);
264 let content = "This is **strong text** here.";
265 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
266 let result = rule.check(&ctx).unwrap();
267
268 assert_eq!(result.len(), 1);
269 assert!(
270 result[0]
271 .message
272 .contains("Strong emphasis should use __ instead of **")
273 );
274 }
275
276 #[test]
277 fn test_consistent_style_first_asterisk() {
278 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
279 let content = "First **strong** then __also strong__.";
280 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
281 let result = rule.check(&ctx).unwrap();
282
283 assert_eq!(result.len(), 1);
285 assert!(
286 result[0]
287 .message
288 .contains("Strong emphasis should use ** instead of __")
289 );
290 }
291
292 #[test]
293 fn test_consistent_style_tie_prefers_asterisk() {
294 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
295 let content = "First __strong__ then **also strong**.";
296 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
297 let result = rule.check(&ctx).unwrap();
298
299 assert_eq!(result.len(), 1);
302 assert!(
303 result[0]
304 .message
305 .contains("Strong emphasis should use ** instead of __")
306 );
307 }
308
309 #[test]
310 fn test_detect_style_asterisk() {
311 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
312 let ctx = LintContext::new(
313 "This has **strong** text.",
314 crate::config::MarkdownFlavor::Standard,
315 None,
316 );
317 let style = rule.detect_style(&ctx);
318
319 assert_eq!(style, Some(StrongStyle::Asterisk));
320 }
321
322 #[test]
323 fn test_detect_style_underscore() {
324 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
325 let ctx = LintContext::new(
326 "This has __strong__ text.",
327 crate::config::MarkdownFlavor::Standard,
328 None,
329 );
330 let style = rule.detect_style(&ctx);
331
332 assert_eq!(style, Some(StrongStyle::Underscore));
333 }
334
335 #[test]
336 fn test_detect_style_none() {
337 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
338 let ctx = LintContext::new("No strong text here.", crate::config::MarkdownFlavor::Standard, None);
339 let style = rule.detect_style(&ctx);
340
341 assert_eq!(style, None);
342 }
343
344 #[test]
345 fn test_strong_in_code_block() {
346 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
347 let content = "```\n__strong__ in code\n```\n__strong__ outside";
348 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
349 let result = rule.check(&ctx).unwrap();
350
351 assert_eq!(result.len(), 1);
353 assert_eq!(result[0].line, 4);
354 }
355
356 #[test]
357 fn test_strong_in_inline_code() {
358 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
359 let content = "Text with `__strong__` in code and __strong__ outside.";
360 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
361 let result = rule.check(&ctx).unwrap();
362
363 assert_eq!(result.len(), 1);
365 }
366
367 #[test]
368 fn test_escaped_strong() {
369 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
370 let content = "This is \\__not strong\\__ but __this is__.";
371 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
372 let result = rule.check(&ctx).unwrap();
373
374 assert_eq!(result.len(), 1);
376 assert_eq!(result[0].line, 1);
377 assert_eq!(result[0].column, 30);
378 }
379
380 #[test]
381 fn test_fix_asterisks_to_underscores() {
382 let rule = MD050StrongStyle::new(StrongStyle::Underscore);
383 let content = "This is **strong** text.";
384 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
385 let fixed = rule.fix(&ctx).unwrap();
386
387 assert_eq!(fixed, "This is __strong__ text.");
388 }
389
390 #[test]
391 fn test_fix_underscores_to_asterisks() {
392 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
393 let content = "This is __strong__ text.";
394 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
395 let fixed = rule.fix(&ctx).unwrap();
396
397 assert_eq!(fixed, "This is **strong** text.");
398 }
399
400 #[test]
401 fn test_fix_multiple_strong() {
402 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
403 let content = "First __strong__ and second __also strong__.";
404 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
405 let fixed = rule.fix(&ctx).unwrap();
406
407 assert_eq!(fixed, "First **strong** and second **also strong**.");
408 }
409
410 #[test]
411 fn test_fix_preserves_code_blocks() {
412 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
413 let content = "```\n__strong__ in code\n```\n__strong__ outside";
414 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
415 let fixed = rule.fix(&ctx).unwrap();
416
417 assert_eq!(fixed, "```\n__strong__ in code\n```\n**strong** outside");
418 }
419
420 #[test]
421 fn test_multiline_content() {
422 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
423 let content = "Line 1 with __strong__\nLine 2 with __another__\nLine 3 normal";
424 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
425 let result = rule.check(&ctx).unwrap();
426
427 assert_eq!(result.len(), 2);
428 assert_eq!(result[0].line, 1);
429 assert_eq!(result[1].line, 2);
430 }
431
432 #[test]
433 fn test_nested_emphasis() {
434 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
435 let content = "This has __strong with *emphasis* inside__.";
436 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
437 let result = rule.check(&ctx).unwrap();
438
439 assert_eq!(result.len(), 1);
440 }
441
442 #[test]
443 fn test_empty_content() {
444 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
445 let content = "";
446 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
447 let result = rule.check(&ctx).unwrap();
448
449 assert_eq!(result.len(), 0);
450 }
451
452 #[test]
453 fn test_default_config() {
454 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
455 let (name, _config) = rule.default_config_section().unwrap();
456 assert_eq!(name, "MD050");
457 }
458
459 #[test]
460 fn test_strong_in_links_not_flagged() {
461 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
462 let content = r#"Instead of assigning to `self.value`, we're relying on the [`__dict__`][__dict__] in our object to hold that value instead.
463
464Hint:
465
466- [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__`")
467
468
469[__dict__]: https://www.pythonmorsels.com/where-are-attributes-stored/"#;
470 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
471 let result = rule.check(&ctx).unwrap();
472
473 assert_eq!(result.len(), 0);
475 }
476
477 #[test]
478 fn test_strong_in_links_vs_outside_links() {
479 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
480 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][]**.
481
482Instead of assigning to `self.value`, we're relying on the [`__dict__`][__dict__] in our object to hold that value instead.
483
484This is __real strong text__ that should be flagged.
485
486[__dict__]: https://www.pythonmorsels.com/where-are-attributes-stored/"#;
487 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
488 let result = rule.check(&ctx).unwrap();
489
490 assert_eq!(result.len(), 1);
492 assert!(
493 result[0]
494 .message
495 .contains("Strong emphasis should use ** instead of __")
496 );
497 assert!(result[0].line > 4); }
500
501 #[test]
502 fn test_front_matter_not_flagged() {
503 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
504 let content = "---\ntitle: What's __init__.py?\nother: __value__\n---\n\nThis __should be flagged__.";
505 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
506 let result = rule.check(&ctx).unwrap();
507
508 assert_eq!(result.len(), 1);
510 assert_eq!(result[0].line, 6);
511 assert!(
512 result[0]
513 .message
514 .contains("Strong emphasis should use ** instead of __")
515 );
516 }
517
518 #[test]
519 fn test_html_tags_not_flagged() {
520 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
521 let content = r#"# Test
522
523This has HTML with underscores:
524
525<iframe src="https://example.com/__init__/__repr__"> </iframe>
526
527This __should be flagged__ as inconsistent."#;
528 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
529 let result = rule.check(&ctx).unwrap();
530
531 assert_eq!(result.len(), 1);
533 assert_eq!(result[0].line, 7);
534 assert!(
535 result[0]
536 .message
537 .contains("Strong emphasis should use ** instead of __")
538 );
539 }
540
541 #[test]
542 fn test_mkdocs_keys_notation_not_flagged() {
543 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
545 let content = "Press ++ctrl+alt+del++ to restart.";
546 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
547 let result = rule.check(&ctx).unwrap();
548
549 assert!(
551 result.is_empty(),
552 "Keys notation should not be flagged as strong emphasis. Got: {result:?}"
553 );
554 }
555
556 #[test]
557 fn test_mkdocs_caret_notation_not_flagged() {
558 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
560 let content = "This is ^^inserted^^ text.";
561 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
562 let result = rule.check(&ctx).unwrap();
563
564 assert!(
565 result.is_empty(),
566 "Insert notation should not be flagged as strong emphasis. Got: {result:?}"
567 );
568 }
569
570 #[test]
571 fn test_mkdocs_mark_notation_not_flagged() {
572 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
574 let content = "This is ==highlighted== text.";
575 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
576 let result = rule.check(&ctx).unwrap();
577
578 assert!(
579 result.is_empty(),
580 "Mark notation should not be flagged as strong emphasis. Got: {result:?}"
581 );
582 }
583
584 #[test]
585 fn test_mkdocs_mixed_content_with_real_strong() {
586 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
588 let content = "Press ++ctrl++ and __underscore strong__ here.";
589 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
590 let result = rule.check(&ctx).unwrap();
591
592 assert_eq!(result.len(), 1, "Expected 1 warning, got: {result:?}");
594 assert!(
595 result[0]
596 .message
597 .contains("Strong emphasis should use ** instead of __")
598 );
599 }
600
601 #[test]
602 fn test_mkdocs_icon_shortcode_not_flagged() {
603 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
605 let content = "Click :material-check: and __this should be flagged__.";
606 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
607 let result = rule.check(&ctx).unwrap();
608
609 assert_eq!(result.len(), 1);
611 assert!(
612 result[0]
613 .message
614 .contains("Strong emphasis should use ** instead of __")
615 );
616 }
617
618 #[test]
619 fn test_math_block_not_flagged() {
620 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
622 let content = r#"# Math Section
623
624$$
625E = mc^2
626x_1 + x_2 = y
627a**b = c
628$$
629
630This __should be flagged__ outside math.
631"#;
632 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
633 let result = rule.check(&ctx).unwrap();
634
635 assert_eq!(result.len(), 1, "Expected 1 warning, got: {result:?}");
637 assert!(result[0].line > 7, "Warning should be on line after math block");
638 }
639
640 #[test]
641 fn test_math_block_with_underscores_not_flagged() {
642 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
644 let content = r#"$$
645x_1 + x_2 + x__3 = y
646\alpha__\beta
647$$
648"#;
649 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
650 let result = rule.check(&ctx).unwrap();
651
652 assert!(
654 result.is_empty(),
655 "Math block content should not be flagged. Got: {result:?}"
656 );
657 }
658
659 #[test]
660 fn test_math_block_with_asterisks_not_flagged() {
661 let rule = MD050StrongStyle::new(StrongStyle::Underscore);
663 let content = r#"$$
664a**b = c
6652 ** 3 = 8
666x***y
667$$
668"#;
669 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
670 let result = rule.check(&ctx).unwrap();
671
672 assert!(
674 result.is_empty(),
675 "Math block content should not be flagged. Got: {result:?}"
676 );
677 }
678
679 #[test]
680 fn test_math_block_fix_preserves_content() {
681 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
683 let content = r#"$$
684x__y = z
685$$
686
687This __word__ should change.
688"#;
689 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
690 let fixed = rule.fix(&ctx).unwrap();
691
692 assert!(fixed.contains("x__y = z"), "Math block content should be preserved");
694 assert!(fixed.contains("**word**"), "Strong outside math should be fixed");
696 }
697
698 #[test]
699 fn test_inline_math_simple() {
700 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
702 let content = "The formula $E = mc^2$ is famous and __this__ is strong.";
703 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
704 let result = rule.check(&ctx).unwrap();
705
706 assert_eq!(
708 result.len(),
709 1,
710 "Expected 1 warning for strong outside math. Got: {result:?}"
711 );
712 }
713
714 #[test]
715 fn test_multiple_math_blocks_and_strong() {
716 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
718 let content = r#"# Document
719
720$$
721a = b
722$$
723
724This __should be flagged__ text.
725
726$$
727c = d
728$$
729"#;
730 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
731 let result = rule.check(&ctx).unwrap();
732
733 assert_eq!(result.len(), 1, "Expected 1 warning. Got: {result:?}");
735 assert!(result[0].message.contains("**"));
736 }
737
738 #[test]
739 fn test_html_tag_skip_consistency_between_check_and_fix() {
740 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
743
744 let content = r#"<a href="__test__">link</a>
745
746This __should be flagged__ text."#;
747 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
748
749 let check_result = rule.check(&ctx).unwrap();
750 let fix_result = rule.fix(&ctx).unwrap();
751
752 assert_eq!(
754 check_result.len(),
755 1,
756 "check() should flag exactly one emphasis outside HTML tags"
757 );
758 assert!(check_result[0].message.contains("**"));
759
760 assert!(
762 fix_result.contains("**should be flagged**"),
763 "fix() should convert the flagged emphasis"
764 );
765 assert!(
766 fix_result.contains("__test__"),
767 "fix() should not modify emphasis inside HTML tags"
768 );
769 }
770
771 #[test]
772 fn test_detect_style_ignores_emphasis_in_inline_code_on_table_lines() {
773 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
776
777 let content = "| `__code__` | **real** |\n| --- | --- |\n| data | data |";
780 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
781
782 let style = rule.detect_style(&ctx);
783 assert_eq!(style, Some(StrongStyle::Asterisk));
785 }
786
787 #[test]
788 fn test_five_underscores_not_flagged() {
789 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
790 let content = "This is a series of underscores: _____";
791 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
792 let result = rule.check(&ctx).unwrap();
793 assert!(
794 result.is_empty(),
795 "_____ should not be flagged as strong emphasis. Got: {result:?}"
796 );
797 }
798
799 #[test]
800 fn test_five_asterisks_not_flagged() {
801 let rule = MD050StrongStyle::new(StrongStyle::Underscore);
802 let content = "This is a series of asterisks: *****";
803 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
804 let result = rule.check(&ctx).unwrap();
805 assert!(
806 result.is_empty(),
807 "***** should not be flagged as strong emphasis. Got: {result:?}"
808 );
809 }
810
811 #[test]
812 fn test_five_underscores_with_frontmatter_not_flagged() {
813 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
814 let content = "---\ntitle: Level 1 heading\n---\n\nThis is a series of underscores: _____\n";
815 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
816 let result = rule.check(&ctx).unwrap();
817 assert!(result.is_empty(), "_____ should not be flagged. Got: {result:?}");
818 }
819
820 #[test]
821 fn test_four_underscores_not_flagged() {
822 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
823 let content = "This is: ____";
824 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
825 let result = rule.check(&ctx).unwrap();
826 assert!(result.is_empty(), "____ should not be flagged. Got: {result:?}");
827 }
828
829 #[test]
830 fn test_four_asterisks_not_flagged() {
831 let rule = MD050StrongStyle::new(StrongStyle::Underscore);
832 let content = "This is: ****";
833 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
834 let result = rule.check(&ctx).unwrap();
835 assert!(result.is_empty(), "**** should not be flagged. Got: {result:?}");
836 }
837
838 #[test]
839 fn test_detect_style_ignores_underscore_sequences() {
840 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
841 let content = "This is: _____ and also **real bold**";
842 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
843 let style = rule.detect_style(&ctx);
844 assert_eq!(style, Some(StrongStyle::Asterisk));
845 }
846
847 #[test]
848 fn test_fix_does_not_modify_underscore_sequences() {
849 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
850 let content = "Some _____ sequence and __real bold__ text.";
851 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
852 let fixed = rule.fix(&ctx).unwrap();
853 assert!(fixed.contains("_____"), "_____ should be preserved");
854 assert!(fixed.contains("**real bold**"), "Real bold should be converted");
855 }
856
857 #[test]
858 fn test_six_or_more_consecutive_markers_not_flagged() {
859 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
860 for count in [6, 7, 8, 10] {
861 let underscores = "_".repeat(count);
862 let asterisks = "*".repeat(count);
863 let content_u = format!("Text with {underscores} here");
864 let content_a = format!("Text with {asterisks} here");
865
866 let ctx_u = LintContext::new(&content_u, crate::config::MarkdownFlavor::Standard, None);
867 let ctx_a = LintContext::new(&content_a, crate::config::MarkdownFlavor::Standard, None);
868
869 let result_u = rule.check(&ctx_u).unwrap();
870 let result_a = rule.check(&ctx_a).unwrap();
871
872 assert!(
873 result_u.is_empty(),
874 "{count} underscores should not be flagged. Got: {result_u:?}"
875 );
876 assert!(
877 result_a.is_empty(),
878 "{count} asterisks should not be flagged. Got: {result_a:?}"
879 );
880 }
881 }
882
883 #[test]
884 fn test_mkdocstrings_block_not_flagged() {
885 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
886 let content = "# Example\n\nWe have here some **bold text**.\n\n::: my_module.MyClass\n options:\n members:\n - __init__\n";
887 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
888 let result = rule.check(&ctx).unwrap();
889
890 assert!(
891 result.is_empty(),
892 "__init__ inside mkdocstrings block should not be flagged. Got: {result:?}"
893 );
894 }
895
896 #[test]
897 fn test_mkdocstrings_block_fix_preserves_content() {
898 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
899 let content = "# Example\n\nWe have here some **bold text**.\n\n::: my_module.MyClass\n options:\n members:\n - __init__\n - __repr__\n";
900 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
901 let fixed = rule.fix(&ctx).unwrap();
902
903 assert!(
904 fixed.contains("__init__"),
905 "__init__ in mkdocstrings block should be preserved"
906 );
907 assert!(
908 fixed.contains("__repr__"),
909 "__repr__ in mkdocstrings block should be preserved"
910 );
911 assert!(fixed.contains("**bold text**"), "Real bold text should be unchanged");
912 }
913
914 #[test]
915 fn test_mkdocstrings_block_with_strong_outside() {
916 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
917 let content = "::: my_module.MyClass\n options:\n members:\n - __init__\n\nThis __should be flagged__ outside.\n";
918 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
919 let result = rule.check(&ctx).unwrap();
920
921 assert_eq!(
922 result.len(),
923 1,
924 "Only strong outside mkdocstrings should be flagged. Got: {result:?}"
925 );
926 assert_eq!(result[0].line, 6);
927 }
928
929 #[test]
930 fn test_thematic_break_not_flagged() {
931 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
932 let content = "Before\n\n*****\n\nAfter";
933 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
934 let result = rule.check(&ctx).unwrap();
935 assert!(
936 result.is_empty(),
937 "Thematic break (*****) should not be flagged. Got: {result:?}"
938 );
939
940 let content2 = "Before\n\n_____\n\nAfter";
941 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
942 let result2 = rule.check(&ctx2).unwrap();
943 assert!(
944 result2.is_empty(),
945 "Thematic break (_____) should not be flagged. Got: {result2:?}"
946 );
947 }
948}