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 crate::impl_rule_config_methods!(MD050Config);
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::lint_context::LintContext;
209
210 #[test]
211 fn test_asterisk_style_with_asterisks() {
212 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
213 let content = "This is **strong text** here.";
214 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
215 let result = rule.check(&ctx).unwrap();
216
217 assert_eq!(result.len(), 0);
218 }
219
220 #[test]
221 fn test_asterisk_style_with_underscores() {
222 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
223 let content = "This is __strong text__ here.";
224 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
225 let result = rule.check(&ctx).unwrap();
226
227 assert_eq!(result.len(), 1);
228 assert!(
229 result[0]
230 .message
231 .contains("Strong emphasis should use ** instead of __")
232 );
233 assert_eq!(result[0].line, 1);
234 assert_eq!(result[0].column, 9);
235 }
236
237 #[test]
238 fn test_underscore_style_with_underscores() {
239 let rule = MD050StrongStyle::new(StrongStyle::Underscore);
240 let content = "This is __strong text__ here.";
241 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
242 let result = rule.check(&ctx).unwrap();
243
244 assert_eq!(result.len(), 0);
245 }
246
247 #[test]
248 fn test_underscore_style_with_asterisks() {
249 let rule = MD050StrongStyle::new(StrongStyle::Underscore);
250 let content = "This is **strong text** here.";
251 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
252 let result = rule.check(&ctx).unwrap();
253
254 assert_eq!(result.len(), 1);
255 assert!(
256 result[0]
257 .message
258 .contains("Strong emphasis should use __ instead of **")
259 );
260 }
261
262 #[test]
263 fn test_consistent_style_first_asterisk() {
264 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
265 let content = "First **strong** then __also strong__.";
266 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
267 let result = rule.check(&ctx).unwrap();
268
269 assert_eq!(result.len(), 1);
271 assert!(
272 result[0]
273 .message
274 .contains("Strong emphasis should use ** instead of __")
275 );
276 }
277
278 #[test]
279 fn test_consistent_style_tie_prefers_asterisk() {
280 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
281 let content = "First __strong__ then **also strong**.";
282 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
283 let result = rule.check(&ctx).unwrap();
284
285 assert_eq!(result.len(), 1);
288 assert!(
289 result[0]
290 .message
291 .contains("Strong emphasis should use ** instead of __")
292 );
293 }
294
295 #[test]
296 fn test_detect_style_asterisk() {
297 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
298 let ctx = LintContext::new(
299 "This has **strong** text.",
300 crate::config::MarkdownFlavor::Standard,
301 None,
302 );
303 let style = rule.detect_style(&ctx);
304
305 assert_eq!(style, Some(StrongStyle::Asterisk));
306 }
307
308 #[test]
309 fn test_detect_style_underscore() {
310 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
311 let ctx = LintContext::new(
312 "This has __strong__ text.",
313 crate::config::MarkdownFlavor::Standard,
314 None,
315 );
316 let style = rule.detect_style(&ctx);
317
318 assert_eq!(style, Some(StrongStyle::Underscore));
319 }
320
321 #[test]
322 fn test_detect_style_none() {
323 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
324 let ctx = LintContext::new("No strong text here.", crate::config::MarkdownFlavor::Standard, None);
325 let style = rule.detect_style(&ctx);
326
327 assert_eq!(style, None);
328 }
329
330 #[test]
331 fn test_strong_in_code_block() {
332 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
333 let content = "```\n__strong__ in code\n```\n__strong__ outside";
334 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
335 let result = rule.check(&ctx).unwrap();
336
337 assert_eq!(result.len(), 1);
339 assert_eq!(result[0].line, 4);
340 }
341
342 #[test]
343 fn test_strong_in_inline_code() {
344 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
345 let content = "Text with `__strong__` in code and __strong__ outside.";
346 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
347 let result = rule.check(&ctx).unwrap();
348
349 assert_eq!(result.len(), 1);
351 }
352
353 #[test]
354 fn test_escaped_strong() {
355 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
356 let content = "This is \\__not strong\\__ but __this is__.";
357 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
358 let result = rule.check(&ctx).unwrap();
359
360 assert_eq!(result.len(), 1);
362 assert_eq!(result[0].line, 1);
363 assert_eq!(result[0].column, 30);
364 }
365
366 #[test]
367 fn test_fix_asterisks_to_underscores() {
368 let rule = MD050StrongStyle::new(StrongStyle::Underscore);
369 let content = "This is **strong** text.";
370 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
371 let fixed = rule.fix(&ctx).unwrap();
372
373 assert_eq!(fixed, "This is __strong__ text.");
374 }
375
376 #[test]
377 fn test_fix_underscores_to_asterisks() {
378 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
379 let content = "This is __strong__ text.";
380 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
381 let fixed = rule.fix(&ctx).unwrap();
382
383 assert_eq!(fixed, "This is **strong** text.");
384 }
385
386 #[test]
387 fn test_fix_multiple_strong() {
388 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
389 let content = "First __strong__ and second __also strong__.";
390 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
391 let fixed = rule.fix(&ctx).unwrap();
392
393 assert_eq!(fixed, "First **strong** and second **also strong**.");
394 }
395
396 #[test]
397 fn test_fix_preserves_code_blocks() {
398 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
399 let content = "```\n__strong__ in code\n```\n__strong__ outside";
400 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
401 let fixed = rule.fix(&ctx).unwrap();
402
403 assert_eq!(fixed, "```\n__strong__ in code\n```\n**strong** outside");
404 }
405
406 #[test]
407 fn test_multiline_content() {
408 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
409 let content = "Line 1 with __strong__\nLine 2 with __another__\nLine 3 normal";
410 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
411 let result = rule.check(&ctx).unwrap();
412
413 assert_eq!(result.len(), 2);
414 assert_eq!(result[0].line, 1);
415 assert_eq!(result[1].line, 2);
416 }
417
418 #[test]
419 fn test_nested_emphasis() {
420 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
421 let content = "This has __strong with *emphasis* inside__.";
422 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
423 let result = rule.check(&ctx).unwrap();
424
425 assert_eq!(result.len(), 1);
426 }
427
428 #[test]
429 fn test_empty_content() {
430 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
431 let content = "";
432 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
433 let result = rule.check(&ctx).unwrap();
434
435 assert_eq!(result.len(), 0);
436 }
437
438 #[test]
439 fn test_default_config() {
440 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
441 let (name, _config) = rule.default_config_section().unwrap();
442 assert_eq!(name, "MD050");
443 }
444
445 #[test]
446 fn test_strong_in_links_not_flagged() {
447 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
448 let content = r#"Instead of assigning to `self.value`, we're relying on the [`__dict__`][__dict__] in our object to hold that value instead.
449
450Hint:
451
452- [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__`")
453
454
455[__dict__]: https://www.pythonmorsels.com/where-are-attributes-stored/"#;
456 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
457 let result = rule.check(&ctx).unwrap();
458
459 assert_eq!(result.len(), 0);
461 }
462
463 #[test]
464 fn test_strong_in_links_vs_outside_links() {
465 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
466 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][]**.
467
468Instead of assigning to `self.value`, we're relying on the [`__dict__`][__dict__] in our object to hold that value instead.
469
470This is __real strong text__ that should be flagged.
471
472[__dict__]: https://www.pythonmorsels.com/where-are-attributes-stored/"#;
473 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
474 let result = rule.check(&ctx).unwrap();
475
476 assert_eq!(result.len(), 1);
478 assert!(
479 result[0]
480 .message
481 .contains("Strong emphasis should use ** instead of __")
482 );
483 assert!(result[0].line > 4); }
486
487 #[test]
488 fn test_front_matter_not_flagged() {
489 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
490 let content = "---\ntitle: What's __init__.py?\nother: __value__\n---\n\nThis __should be flagged__.";
491 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
492 let result = rule.check(&ctx).unwrap();
493
494 assert_eq!(result.len(), 1);
496 assert_eq!(result[0].line, 6);
497 assert!(
498 result[0]
499 .message
500 .contains("Strong emphasis should use ** instead of __")
501 );
502 }
503
504 #[test]
505 fn test_html_tags_not_flagged() {
506 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
507 let content = r#"# Test
508
509This has HTML with underscores:
510
511<iframe src="https://example.com/__init__/__repr__"> </iframe>
512
513This __should be flagged__ as inconsistent."#;
514 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
515 let result = rule.check(&ctx).unwrap();
516
517 assert_eq!(result.len(), 1);
519 assert_eq!(result[0].line, 7);
520 assert!(
521 result[0]
522 .message
523 .contains("Strong emphasis should use ** instead of __")
524 );
525 }
526
527 #[test]
528 fn test_mkdocs_keys_notation_not_flagged() {
529 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
531 let content = "Press ++ctrl+alt+del++ to restart.";
532 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
533 let result = rule.check(&ctx).unwrap();
534
535 assert!(
537 result.is_empty(),
538 "Keys notation should not be flagged as strong emphasis. Got: {result:?}"
539 );
540 }
541
542 #[test]
543 fn test_mkdocs_caret_notation_not_flagged() {
544 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
546 let content = "This is ^^inserted^^ text.";
547 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
548 let result = rule.check(&ctx).unwrap();
549
550 assert!(
551 result.is_empty(),
552 "Insert notation should not be flagged as strong emphasis. Got: {result:?}"
553 );
554 }
555
556 #[test]
557 fn test_mkdocs_mark_notation_not_flagged() {
558 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
560 let content = "This is ==highlighted== 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 "Mark notation should not be flagged as strong emphasis. Got: {result:?}"
567 );
568 }
569
570 #[test]
571 fn test_mkdocs_mixed_content_with_real_strong() {
572 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
574 let content = "Press ++ctrl++ and __underscore strong__ here.";
575 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
576 let result = rule.check(&ctx).unwrap();
577
578 assert_eq!(result.len(), 1, "Expected 1 warning, got: {result:?}");
580 assert!(
581 result[0]
582 .message
583 .contains("Strong emphasis should use ** instead of __")
584 );
585 }
586
587 #[test]
588 fn test_mkdocs_icon_shortcode_not_flagged() {
589 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
591 let content = "Click :material-check: and __this should be flagged__.";
592 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
593 let result = rule.check(&ctx).unwrap();
594
595 assert_eq!(result.len(), 1);
597 assert!(
598 result[0]
599 .message
600 .contains("Strong emphasis should use ** instead of __")
601 );
602 }
603
604 #[test]
605 fn test_math_block_not_flagged() {
606 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
608 let content = r#"# Math Section
609
610$$
611E = mc^2
612x_1 + x_2 = y
613a**b = c
614$$
615
616This __should be flagged__ outside math.
617"#;
618 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
619 let result = rule.check(&ctx).unwrap();
620
621 assert_eq!(result.len(), 1, "Expected 1 warning, got: {result:?}");
623 assert!(result[0].line > 7, "Warning should be on line after math block");
624 }
625
626 #[test]
627 fn test_math_block_with_underscores_not_flagged() {
628 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
630 let content = r#"$$
631x_1 + x_2 + x__3 = y
632\alpha__\beta
633$$
634"#;
635 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
636 let result = rule.check(&ctx).unwrap();
637
638 assert!(
640 result.is_empty(),
641 "Math block content should not be flagged. Got: {result:?}"
642 );
643 }
644
645 #[test]
646 fn test_math_block_with_asterisks_not_flagged() {
647 let rule = MD050StrongStyle::new(StrongStyle::Underscore);
649 let content = r#"$$
650a**b = c
6512 ** 3 = 8
652x***y
653$$
654"#;
655 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
656 let result = rule.check(&ctx).unwrap();
657
658 assert!(
660 result.is_empty(),
661 "Math block content should not be flagged. Got: {result:?}"
662 );
663 }
664
665 #[test]
666 fn test_math_block_fix_preserves_content() {
667 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
669 let content = r#"$$
670x__y = z
671$$
672
673This __word__ should change.
674"#;
675 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
676 let fixed = rule.fix(&ctx).unwrap();
677
678 assert!(fixed.contains("x__y = z"), "Math block content should be preserved");
680 assert!(fixed.contains("**word**"), "Strong outside math should be fixed");
682 }
683
684 #[test]
685 fn test_inline_math_simple() {
686 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
688 let content = "The formula $E = mc^2$ is famous and __this__ is strong.";
689 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
690 let result = rule.check(&ctx).unwrap();
691
692 assert_eq!(
694 result.len(),
695 1,
696 "Expected 1 warning for strong outside math. Got: {result:?}"
697 );
698 }
699
700 #[test]
701 fn test_multiple_math_blocks_and_strong() {
702 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
704 let content = r#"# Document
705
706$$
707a = b
708$$
709
710This __should be flagged__ text.
711
712$$
713c = d
714$$
715"#;
716 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
717 let result = rule.check(&ctx).unwrap();
718
719 assert_eq!(result.len(), 1, "Expected 1 warning. Got: {result:?}");
721 assert!(result[0].message.contains("**"));
722 }
723
724 #[test]
725 fn test_html_tag_skip_consistency_between_check_and_fix() {
726 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
729
730 let content = r#"<a href="__test__">link</a>
731
732This __should be flagged__ text."#;
733 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
734
735 let check_result = rule.check(&ctx).unwrap();
736 let fix_result = rule.fix(&ctx).unwrap();
737
738 assert_eq!(
740 check_result.len(),
741 1,
742 "check() should flag exactly one emphasis outside HTML tags"
743 );
744 assert!(check_result[0].message.contains("**"));
745
746 assert!(
748 fix_result.contains("**should be flagged**"),
749 "fix() should convert the flagged emphasis"
750 );
751 assert!(
752 fix_result.contains("__test__"),
753 "fix() should not modify emphasis inside HTML tags"
754 );
755 }
756
757 #[test]
758 fn test_detect_style_ignores_emphasis_in_inline_code_on_table_lines() {
759 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
762
763 let content = "| `__code__` | **real** |\n| --- | --- |\n| data | data |";
766 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
767
768 let style = rule.detect_style(&ctx);
769 assert_eq!(style, Some(StrongStyle::Asterisk));
771 }
772
773 #[test]
774 fn test_five_underscores_not_flagged() {
775 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
776 let content = "This is a series of underscores: _____";
777 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
778 let result = rule.check(&ctx).unwrap();
779 assert!(
780 result.is_empty(),
781 "_____ should not be flagged as strong emphasis. Got: {result:?}"
782 );
783 }
784
785 #[test]
786 fn test_five_asterisks_not_flagged() {
787 let rule = MD050StrongStyle::new(StrongStyle::Underscore);
788 let content = "This is a series of asterisks: *****";
789 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
790 let result = rule.check(&ctx).unwrap();
791 assert!(
792 result.is_empty(),
793 "***** should not be flagged as strong emphasis. Got: {result:?}"
794 );
795 }
796
797 #[test]
798 fn test_five_underscores_with_frontmatter_not_flagged() {
799 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
800 let content = "---\ntitle: Level 1 heading\n---\n\nThis is a series of underscores: _____\n";
801 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
802 let result = rule.check(&ctx).unwrap();
803 assert!(result.is_empty(), "_____ should not be flagged. Got: {result:?}");
804 }
805
806 #[test]
807 fn test_four_underscores_not_flagged() {
808 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
809 let content = "This is: ____";
810 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
811 let result = rule.check(&ctx).unwrap();
812 assert!(result.is_empty(), "____ should not be flagged. Got: {result:?}");
813 }
814
815 #[test]
816 fn test_four_asterisks_not_flagged() {
817 let rule = MD050StrongStyle::new(StrongStyle::Underscore);
818 let content = "This is: ****";
819 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
820 let result = rule.check(&ctx).unwrap();
821 assert!(result.is_empty(), "**** should not be flagged. Got: {result:?}");
822 }
823
824 #[test]
825 fn test_detect_style_ignores_underscore_sequences() {
826 let rule = MD050StrongStyle::new(StrongStyle::Consistent);
827 let content = "This is: _____ and also **real bold**";
828 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
829 let style = rule.detect_style(&ctx);
830 assert_eq!(style, Some(StrongStyle::Asterisk));
831 }
832
833 #[test]
834 fn test_fix_does_not_modify_underscore_sequences() {
835 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
836 let content = "Some _____ sequence and __real bold__ text.";
837 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
838 let fixed = rule.fix(&ctx).unwrap();
839 assert!(fixed.contains("_____"), "_____ should be preserved");
840 assert!(fixed.contains("**real bold**"), "Real bold should be converted");
841 }
842
843 #[test]
844 fn test_six_or_more_consecutive_markers_not_flagged() {
845 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
846 for count in [6, 7, 8, 10] {
847 let underscores = "_".repeat(count);
848 let asterisks = "*".repeat(count);
849 let content_u = format!("Text with {underscores} here");
850 let content_a = format!("Text with {asterisks} here");
851
852 let ctx_u = LintContext::new(&content_u, crate::config::MarkdownFlavor::Standard, None);
853 let ctx_a = LintContext::new(&content_a, crate::config::MarkdownFlavor::Standard, None);
854
855 let result_u = rule.check(&ctx_u).unwrap();
856 let result_a = rule.check(&ctx_a).unwrap();
857
858 assert!(
859 result_u.is_empty(),
860 "{count} underscores should not be flagged. Got: {result_u:?}"
861 );
862 assert!(
863 result_a.is_empty(),
864 "{count} asterisks should not be flagged. Got: {result_a:?}"
865 );
866 }
867 }
868
869 #[test]
870 fn test_mkdocstrings_block_not_flagged() {
871 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
872 let content = "# Example\n\nWe have here some **bold text**.\n\n::: my_module.MyClass\n options:\n members:\n - __init__\n";
873 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
874 let result = rule.check(&ctx).unwrap();
875
876 assert!(
877 result.is_empty(),
878 "__init__ inside mkdocstrings block should not be flagged. Got: {result:?}"
879 );
880 }
881
882 #[test]
883 fn test_mkdocstrings_block_fix_preserves_content() {
884 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
885 let content = "# Example\n\nWe have here some **bold text**.\n\n::: my_module.MyClass\n options:\n members:\n - __init__\n - __repr__\n";
886 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
887 let fixed = rule.fix(&ctx).unwrap();
888
889 assert!(
890 fixed.contains("__init__"),
891 "__init__ in mkdocstrings block should be preserved"
892 );
893 assert!(
894 fixed.contains("__repr__"),
895 "__repr__ in mkdocstrings block should be preserved"
896 );
897 assert!(fixed.contains("**bold text**"), "Real bold text should be unchanged");
898 }
899
900 #[test]
901 fn test_mkdocstrings_block_with_strong_outside() {
902 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
903 let content = "::: my_module.MyClass\n options:\n members:\n - __init__\n\nThis __should be flagged__ outside.\n";
904 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
905 let result = rule.check(&ctx).unwrap();
906
907 assert_eq!(
908 result.len(),
909 1,
910 "Only strong outside mkdocstrings should be flagged. Got: {result:?}"
911 );
912 assert_eq!(result[0].line, 6);
913 }
914
915 #[test]
916 fn test_thematic_break_not_flagged() {
917 let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
918 let content = "Before\n\n*****\n\nAfter";
919 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
920 let result = rule.check(&ctx).unwrap();
921 assert!(
922 result.is_empty(),
923 "Thematic break (*****) should not be flagged. Got: {result:?}"
924 );
925
926 let content2 = "Before\n\n_____\n\nAfter";
927 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
928 let result2 = rule.check(&ctx2).unwrap();
929 assert!(
930 result2.is_empty(),
931 "Thematic break (_____) should not be flagged. Got: {result2:?}"
932 );
933 }
934}