use crate::utils::range_utils::calculate_match_range;
use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
use crate::rules::strong_style::StrongStyle;
use crate::utils::code_block_utils::StrongSpanDetail;
use crate::utils::skip_context::{is_in_jsx_expression, is_in_math_context, is_in_mdx_comment, is_in_mkdocs_markup};
fn is_in_inline_code_on_line(line: &str, byte_pos: usize) -> bool {
let bytes = line.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'`' {
let open_start = i;
let mut backtick_count = 0;
while i < bytes.len() && bytes[i] == b'`' {
backtick_count += 1;
i += 1;
}
let mut j = i;
while j < bytes.len() {
if bytes[j] == b'`' {
let mut close_count = 0;
while j < bytes.len() && bytes[j] == b'`' {
close_count += 1;
j += 1;
}
if close_count == backtick_count {
if byte_pos >= open_start && byte_pos < j {
return true;
}
i = j;
break;
}
} else {
j += 1;
}
}
if j >= bytes.len() {
break;
}
} else {
i += 1;
}
}
false
}
fn span_style(span: &StrongSpanDetail) -> StrongStyle {
if span.is_asterisk {
StrongStyle::Asterisk
} else {
StrongStyle::Underscore
}
}
mod md050_config;
use md050_config::MD050Config;
#[derive(Debug, Default, Clone)]
pub struct MD050StrongStyle {
config: MD050Config,
}
impl MD050StrongStyle {
pub fn new(style: StrongStyle) -> Self {
Self {
config: MD050Config { style },
}
}
pub fn from_config_struct(config: MD050Config) -> Self {
Self { config }
}
fn is_in_link(ctx: &crate::lint_context::LintContext, byte_pos: usize) -> bool {
ctx.is_in_link(byte_pos)
}
fn is_in_html_tag(html_tags: &[crate::lint_context::HtmlTag], byte_pos: usize) -> bool {
let idx = html_tags.partition_point(|tag| tag.byte_offset <= byte_pos);
idx > 0 && byte_pos < html_tags[idx - 1].byte_end
}
fn is_in_html_code_content(code_ranges: &[(usize, usize)], byte_pos: usize) -> bool {
let idx = code_ranges.partition_point(|&(start, _)| start <= byte_pos);
idx > 0 && byte_pos < code_ranges[idx - 1].1
}
fn compute_html_code_ranges(html_tags: &[crate::lint_context::HtmlTag]) -> Vec<(usize, usize)> {
let mut ranges = Vec::new();
let mut open_code_end: Option<usize> = None;
for tag in html_tags {
if tag.tag_name == "code" {
if tag.is_self_closing {
continue;
} else if !tag.is_closing {
open_code_end = Some(tag.byte_end);
} else if tag.is_closing {
if let Some(start) = open_code_end {
ranges.push((start, tag.byte_offset));
}
open_code_end = None;
}
}
}
if let Some(start) = open_code_end {
ranges.push((start, usize::MAX));
}
ranges
}
fn should_skip_span(
&self,
ctx: &crate::lint_context::LintContext,
html_tags: &[crate::lint_context::HtmlTag],
html_code_ranges: &[(usize, usize)],
span_start: usize,
) -> bool {
let lines = ctx.raw_lines();
let (line_num, col) = ctx.offset_to_line_col(span_start);
if ctx
.line_info(line_num)
.map(|info| info.in_front_matter || info.in_mkdocstrings)
.unwrap_or(false)
{
return true;
}
let in_mkdocs_markup = lines
.get(line_num.saturating_sub(1))
.is_some_and(|line| is_in_mkdocs_markup(line, col.saturating_sub(1), ctx.flavor));
let in_inline_code = lines
.get(line_num.saturating_sub(1))
.is_some_and(|line| is_in_inline_code_on_line(line, col.saturating_sub(1)));
ctx.is_in_code_block_or_span(span_start)
|| in_inline_code
|| Self::is_in_link(ctx, span_start)
|| Self::is_in_html_tag(html_tags, span_start)
|| Self::is_in_html_code_content(html_code_ranges, span_start)
|| in_mkdocs_markup
|| is_in_math_context(ctx, span_start)
|| is_in_jsx_expression(ctx, span_start)
|| is_in_mdx_comment(ctx, span_start)
}
#[cfg(test)]
fn detect_style(&self, ctx: &crate::lint_context::LintContext) -> Option<StrongStyle> {
let html_tags = ctx.html_tags();
let html_code_ranges = Self::compute_html_code_ranges(&html_tags);
self.detect_style_from_spans(ctx, &html_tags, &html_code_ranges, &ctx.strong_spans)
}
fn detect_style_from_spans(
&self,
ctx: &crate::lint_context::LintContext,
html_tags: &[crate::lint_context::HtmlTag],
html_code_ranges: &[(usize, usize)],
spans: &[StrongSpanDetail],
) -> Option<StrongStyle> {
let mut asterisk_count = 0;
let mut underscore_count = 0;
for span in spans {
if self.should_skip_span(ctx, html_tags, html_code_ranges, span.start) {
continue;
}
match span_style(span) {
StrongStyle::Asterisk => asterisk_count += 1,
StrongStyle::Underscore => underscore_count += 1,
StrongStyle::Consistent => {}
}
}
match (asterisk_count, underscore_count) {
(0, 0) => None,
(_, 0) => Some(StrongStyle::Asterisk),
(0, _) => Some(StrongStyle::Underscore),
(a, u) => {
if a >= u {
Some(StrongStyle::Asterisk)
} else {
Some(StrongStyle::Underscore)
}
}
}
}
}
impl Rule for MD050StrongStyle {
fn name(&self) -> &'static str {
"MD050"
}
fn description(&self) -> &'static str {
"Strong emphasis style should be consistent"
}
fn category(&self) -> RuleCategory {
RuleCategory::Emphasis
}
fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
let content = ctx.content;
let line_index = &ctx.line_index;
let lines = ctx.raw_lines();
let mut warnings = Vec::new();
let spans = &ctx.strong_spans;
let html_tags = ctx.html_tags();
let html_code_ranges = Self::compute_html_code_ranges(&html_tags);
let target_style = match self.config.style {
StrongStyle::Consistent => {
let mut asterisk_count = 0usize;
let mut underscore_count = 0usize;
for span in spans {
if span.end - span.start < 4 {
continue;
}
match span_style(span) {
StrongStyle::Asterisk => asterisk_count += 1,
StrongStyle::Underscore => underscore_count += 1,
StrongStyle::Consistent => {}
}
}
match (asterisk_count, underscore_count) {
(0, 0) => StrongStyle::Asterisk,
(_, 0) => StrongStyle::Asterisk,
(0, _) => StrongStyle::Underscore,
(a, u) => {
if a >= u {
StrongStyle::Asterisk
} else {
StrongStyle::Underscore
}
}
}
}
_ => self.config.style,
};
for span in spans {
if span_style(span) == target_style {
continue;
}
if span.end - span.start < 4 {
continue;
}
if self.should_skip_span(ctx, &html_tags, &html_code_ranges, span.start) {
continue;
}
let (line_num, _col) = ctx.offset_to_line_col(span.start);
let line_start = line_index.get_line_start_byte(line_num).unwrap_or(0);
let line_content = lines.get(line_num - 1).unwrap_or(&"");
let match_start_in_line = span.start - line_start;
let match_len = span.end - span.start;
let inner_text = &content[span.start + 2..span.end - 2];
let message = match target_style {
StrongStyle::Asterisk => "Strong emphasis should use ** instead of __",
StrongStyle::Underscore => "Strong emphasis should use __ instead of **",
StrongStyle::Consistent => "Strong emphasis should use ** instead of __",
};
let (start_line, start_col, end_line, end_col) =
calculate_match_range(line_num, line_content, match_start_in_line, match_len);
warnings.push(LintWarning {
rule_name: Some(self.name().to_string()),
line: start_line,
column: start_col,
end_line,
end_column: end_col,
message: message.to_string(),
severity: Severity::Warning,
fix: Some(Fix {
range: span.start..span.end,
replacement: match target_style {
StrongStyle::Asterisk => format!("**{inner_text}**"),
StrongStyle::Underscore => format!("__{inner_text}__"),
StrongStyle::Consistent => format!("**{inner_text}**"),
},
}),
});
}
Ok(warnings)
}
fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
let content = ctx.content;
let spans = &ctx.strong_spans;
let html_tags = ctx.html_tags();
let html_code_ranges = Self::compute_html_code_ranges(&html_tags);
let target_style = match self.config.style {
StrongStyle::Consistent => self
.detect_style_from_spans(ctx, &html_tags, &html_code_ranges, spans)
.unwrap_or(StrongStyle::Asterisk),
_ => self.config.style,
};
let matches: Vec<std::ops::Range<usize>> = spans
.iter()
.filter(|span| span.end - span.start >= 4)
.filter(|span| span_style(span) != target_style)
.filter(|span| !self.should_skip_span(ctx, &html_tags, &html_code_ranges, span.start))
.filter(|span| {
let (line_num, _) = ctx.offset_to_line_col(span.start);
!ctx.inline_config().is_rule_disabled(self.name(), line_num)
})
.map(|span| span.start..span.end)
.collect();
let mut result = content.to_string();
for range in matches.into_iter().rev() {
let text = &result[range.start + 2..range.end - 2];
let replacement = match target_style {
StrongStyle::Asterisk => format!("**{text}**"),
StrongStyle::Underscore => format!("__{text}__"),
StrongStyle::Consistent => format!("**{text}**"),
};
result.replace_range(range, &replacement);
}
Ok(result)
}
fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
ctx.content.is_empty() || !ctx.likely_has_emphasis()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn default_config_section(&self) -> Option<(String, toml::Value)> {
let json_value = serde_json::to_value(&self.config).ok()?;
Some((
self.name().to_string(),
crate::rule_config_serde::json_to_toml_value(&json_value)?,
))
}
fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
where
Self: Sized,
{
let rule_config = crate::rule_config_serde::load_rule_config::<MD050Config>(config);
Box::new(Self::from_config_struct(rule_config))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lint_context::LintContext;
#[test]
fn test_asterisk_style_with_asterisks() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "This is **strong text** here.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_asterisk_style_with_underscores() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "This is __strong text__ here.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(
result[0]
.message
.contains("Strong emphasis should use ** instead of __")
);
assert_eq!(result[0].line, 1);
assert_eq!(result[0].column, 9);
}
#[test]
fn test_underscore_style_with_underscores() {
let rule = MD050StrongStyle::new(StrongStyle::Underscore);
let content = "This is __strong text__ here.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_underscore_style_with_asterisks() {
let rule = MD050StrongStyle::new(StrongStyle::Underscore);
let content = "This is **strong text** here.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(
result[0]
.message
.contains("Strong emphasis should use __ instead of **")
);
}
#[test]
fn test_consistent_style_first_asterisk() {
let rule = MD050StrongStyle::new(StrongStyle::Consistent);
let content = "First **strong** then __also strong__.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(
result[0]
.message
.contains("Strong emphasis should use ** instead of __")
);
}
#[test]
fn test_consistent_style_tie_prefers_asterisk() {
let rule = MD050StrongStyle::new(StrongStyle::Consistent);
let content = "First __strong__ then **also strong**.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(
result[0]
.message
.contains("Strong emphasis should use ** instead of __")
);
}
#[test]
fn test_detect_style_asterisk() {
let rule = MD050StrongStyle::new(StrongStyle::Consistent);
let ctx = LintContext::new(
"This has **strong** text.",
crate::config::MarkdownFlavor::Standard,
None,
);
let style = rule.detect_style(&ctx);
assert_eq!(style, Some(StrongStyle::Asterisk));
}
#[test]
fn test_detect_style_underscore() {
let rule = MD050StrongStyle::new(StrongStyle::Consistent);
let ctx = LintContext::new(
"This has __strong__ text.",
crate::config::MarkdownFlavor::Standard,
None,
);
let style = rule.detect_style(&ctx);
assert_eq!(style, Some(StrongStyle::Underscore));
}
#[test]
fn test_detect_style_none() {
let rule = MD050StrongStyle::new(StrongStyle::Consistent);
let ctx = LintContext::new("No strong text here.", crate::config::MarkdownFlavor::Standard, None);
let style = rule.detect_style(&ctx);
assert_eq!(style, None);
}
#[test]
fn test_strong_in_code_block() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "```\n__strong__ in code\n```\n__strong__ outside";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].line, 4);
}
#[test]
fn test_strong_in_inline_code() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "Text with `__strong__` in code and __strong__ outside.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_escaped_strong() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "This is \\__not strong\\__ but __this is__.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].line, 1);
assert_eq!(result[0].column, 30);
}
#[test]
fn test_fix_asterisks_to_underscores() {
let rule = MD050StrongStyle::new(StrongStyle::Underscore);
let content = "This is **strong** text.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "This is __strong__ text.");
}
#[test]
fn test_fix_underscores_to_asterisks() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "This is __strong__ text.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "This is **strong** text.");
}
#[test]
fn test_fix_multiple_strong() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "First __strong__ and second __also strong__.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "First **strong** and second **also strong**.");
}
#[test]
fn test_fix_preserves_code_blocks() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "```\n__strong__ in code\n```\n__strong__ outside";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "```\n__strong__ in code\n```\n**strong** outside");
}
#[test]
fn test_multiline_content() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "Line 1 with __strong__\nLine 2 with __another__\nLine 3 normal";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].line, 1);
assert_eq!(result[1].line, 2);
}
#[test]
fn test_nested_emphasis() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "This has __strong with *emphasis* inside__.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_empty_content() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_default_config() {
let rule = MD050StrongStyle::new(StrongStyle::Consistent);
let (name, _config) = rule.default_config_section().unwrap();
assert_eq!(name, "MD050");
}
#[test]
fn test_strong_in_links_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = r#"Instead of assigning to `self.value`, we're relying on the [`__dict__`][__dict__] in our object to hold that value instead.
Hint:
- [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__`")
[__dict__]: https://www.pythonmorsels.com/where-are-attributes-stored/"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_strong_in_links_vs_outside_links() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
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][]**.
Instead of assigning to `self.value`, we're relying on the [`__dict__`][__dict__] in our object to hold that value instead.
This is __real strong text__ that should be flagged.
[__dict__]: https://www.pythonmorsels.com/where-are-attributes-stored/"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(
result[0]
.message
.contains("Strong emphasis should use ** instead of __")
);
assert!(result[0].line > 4); }
#[test]
fn test_front_matter_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "---\ntitle: What's __init__.py?\nother: __value__\n---\n\nThis __should be flagged__.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].line, 6);
assert!(
result[0]
.message
.contains("Strong emphasis should use ** instead of __")
);
}
#[test]
fn test_html_tags_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = r#"# Test
This has HTML with underscores:
<iframe src="https://example.com/__init__/__repr__"> </iframe>
This __should be flagged__ as inconsistent."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].line, 7);
assert!(
result[0]
.message
.contains("Strong emphasis should use ** instead of __")
);
}
#[test]
fn test_mkdocs_keys_notation_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "Press ++ctrl+alt+del++ to restart.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Keys notation should not be flagged as strong emphasis. Got: {result:?}"
);
}
#[test]
fn test_mkdocs_caret_notation_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "This is ^^inserted^^ text.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Insert notation should not be flagged as strong emphasis. Got: {result:?}"
);
}
#[test]
fn test_mkdocs_mark_notation_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "This is ==highlighted== text.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Mark notation should not be flagged as strong emphasis. Got: {result:?}"
);
}
#[test]
fn test_mkdocs_mixed_content_with_real_strong() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "Press ++ctrl++ and __underscore strong__ here.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Expected 1 warning, got: {result:?}");
assert!(
result[0]
.message
.contains("Strong emphasis should use ** instead of __")
);
}
#[test]
fn test_mkdocs_icon_shortcode_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "Click :material-check: and __this should be flagged__.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(
result[0]
.message
.contains("Strong emphasis should use ** instead of __")
);
}
#[test]
fn test_math_block_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = r#"# Math Section
$$
E = mc^2
x_1 + x_2 = y
a**b = c
$$
This __should be flagged__ outside math.
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Expected 1 warning, got: {result:?}");
assert!(result[0].line > 7, "Warning should be on line after math block");
}
#[test]
fn test_math_block_with_underscores_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = r#"$$
x_1 + x_2 + x__3 = y
\alpha__\beta
$$
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Math block content should not be flagged. Got: {result:?}"
);
}
#[test]
fn test_math_block_with_asterisks_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Underscore);
let content = r#"$$
a**b = c
2 ** 3 = 8
x***y
$$
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Math block content should not be flagged. Got: {result:?}"
);
}
#[test]
fn test_math_block_fix_preserves_content() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = r#"$$
x__y = z
$$
This __word__ should change.
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("x__y = z"), "Math block content should be preserved");
assert!(fixed.contains("**word**"), "Strong outside math should be fixed");
}
#[test]
fn test_inline_math_simple() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "The formula $E = mc^2$ is famous and __this__ is strong.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Expected 1 warning for strong outside math. Got: {result:?}"
);
}
#[test]
fn test_multiple_math_blocks_and_strong() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = r#"# Document
$$
a = b
$$
This __should be flagged__ text.
$$
c = d
$$
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Expected 1 warning. Got: {result:?}");
assert!(result[0].message.contains("**"));
}
#[test]
fn test_html_tag_skip_consistency_between_check_and_fix() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = r#"<a href="__test__">link</a>
This __should be flagged__ text."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let check_result = rule.check(&ctx).unwrap();
let fix_result = rule.fix(&ctx).unwrap();
assert_eq!(
check_result.len(),
1,
"check() should flag exactly one emphasis outside HTML tags"
);
assert!(check_result[0].message.contains("**"));
assert!(
fix_result.contains("**should be flagged**"),
"fix() should convert the flagged emphasis"
);
assert!(
fix_result.contains("__test__"),
"fix() should not modify emphasis inside HTML tags"
);
}
#[test]
fn test_detect_style_ignores_emphasis_in_inline_code_on_table_lines() {
let rule = MD050StrongStyle::new(StrongStyle::Consistent);
let content = "| `__code__` | **real** |\n| --- | --- |\n| data | data |";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let style = rule.detect_style(&ctx);
assert_eq!(style, Some(StrongStyle::Asterisk));
}
#[test]
fn test_five_underscores_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "This is a series of underscores: _____";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"_____ should not be flagged as strong emphasis. Got: {result:?}"
);
}
#[test]
fn test_five_asterisks_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Underscore);
let content = "This is a series of asterisks: *****";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"***** should not be flagged as strong emphasis. Got: {result:?}"
);
}
#[test]
fn test_five_underscores_with_frontmatter_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "---\ntitle: Level 1 heading\n---\n\nThis is a series of underscores: _____\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "_____ should not be flagged. Got: {result:?}");
}
#[test]
fn test_four_underscores_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "This is: ____";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "____ should not be flagged. Got: {result:?}");
}
#[test]
fn test_four_asterisks_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Underscore);
let content = "This is: ****";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "**** should not be flagged. Got: {result:?}");
}
#[test]
fn test_detect_style_ignores_underscore_sequences() {
let rule = MD050StrongStyle::new(StrongStyle::Consistent);
let content = "This is: _____ and also **real bold**";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let style = rule.detect_style(&ctx);
assert_eq!(style, Some(StrongStyle::Asterisk));
}
#[test]
fn test_fix_does_not_modify_underscore_sequences() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "Some _____ sequence and __real bold__ text.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("_____"), "_____ should be preserved");
assert!(fixed.contains("**real bold**"), "Real bold should be converted");
}
#[test]
fn test_six_or_more_consecutive_markers_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
for count in [6, 7, 8, 10] {
let underscores = "_".repeat(count);
let asterisks = "*".repeat(count);
let content_u = format!("Text with {underscores} here");
let content_a = format!("Text with {asterisks} here");
let ctx_u = LintContext::new(&content_u, crate::config::MarkdownFlavor::Standard, None);
let ctx_a = LintContext::new(&content_a, crate::config::MarkdownFlavor::Standard, None);
let result_u = rule.check(&ctx_u).unwrap();
let result_a = rule.check(&ctx_a).unwrap();
assert!(
result_u.is_empty(),
"{count} underscores should not be flagged. Got: {result_u:?}"
);
assert!(
result_a.is_empty(),
"{count} asterisks should not be flagged. Got: {result_a:?}"
);
}
}
#[test]
fn test_mkdocstrings_block_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "# Example\n\nWe have here some **bold text**.\n\n::: my_module.MyClass\n options:\n members:\n - __init__\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"__init__ inside mkdocstrings block should not be flagged. Got: {result:?}"
);
}
#[test]
fn test_mkdocstrings_block_fix_preserves_content() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "# Example\n\nWe have here some **bold text**.\n\n::: my_module.MyClass\n options:\n members:\n - __init__\n - __repr__\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains("__init__"),
"__init__ in mkdocstrings block should be preserved"
);
assert!(
fixed.contains("__repr__"),
"__repr__ in mkdocstrings block should be preserved"
);
assert!(fixed.contains("**bold text**"), "Real bold text should be unchanged");
}
#[test]
fn test_mkdocstrings_block_with_strong_outside() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "::: my_module.MyClass\n options:\n members:\n - __init__\n\nThis __should be flagged__ outside.\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Only strong outside mkdocstrings should be flagged. Got: {result:?}"
);
assert_eq!(result[0].line, 6);
}
#[test]
fn test_thematic_break_not_flagged() {
let rule = MD050StrongStyle::new(StrongStyle::Asterisk);
let content = "Before\n\n*****\n\nAfter";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Thematic break (*****) should not be flagged. Got: {result:?}"
);
let content2 = "Before\n\n_____\n\nAfter";
let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
let result2 = rule.check(&ctx2).unwrap();
assert!(
result2.is_empty(),
"Thematic break (_____) should not be flagged. Got: {result2:?}"
);
}
}