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