use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
use crate::utils::calculate_indentation_width_default;
use crate::utils::mkdocs_admonitions;
use crate::utils::mkdocs_footnotes;
use crate::utils::mkdocs_tabs;
use crate::utils::range_utils::calculate_line_range;
use toml;
mod md046_config;
pub use md046_config::CodeBlockStyle;
use md046_config::MD046Config;
struct IndentContext<'a> {
in_list_context: &'a [bool],
in_tab_context: &'a [bool],
in_admonition_context: &'a [bool],
in_jsx_context: &'a [bool],
}
#[derive(Clone)]
pub struct MD046CodeBlockStyle {
config: MD046Config,
}
impl MD046CodeBlockStyle {
pub fn new(style: CodeBlockStyle) -> Self {
Self {
config: MD046Config { style },
}
}
pub fn from_config_struct(config: MD046Config) -> Self {
Self { config }
}
fn has_valid_fence_indent(line: &str) -> bool {
calculate_indentation_width_default(line) < 4
}
fn is_fenced_code_block_start(&self, line: &str) -> bool {
if !Self::has_valid_fence_indent(line) {
return false;
}
let trimmed = line.trim_start();
trimmed.starts_with("```") || trimmed.starts_with("~~~")
}
fn is_list_item(&self, line: &str) -> bool {
let trimmed = line.trim_start();
(trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ "))
|| (trimmed.len() > 2
&& trimmed.chars().next().unwrap().is_numeric()
&& (trimmed.contains(". ") || trimmed.contains(") ")))
}
fn is_footnote_definition(&self, line: &str) -> bool {
let trimmed = line.trim_start();
if !trimmed.starts_with("[^") || trimmed.len() < 5 {
return false;
}
if let Some(close_bracket_pos) = trimmed.find("]:")
&& close_bracket_pos > 2
{
let label = &trimmed[2..close_bracket_pos];
if label.trim().is_empty() {
return false;
}
if label.contains('\r') {
return false;
}
if label.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
return true;
}
}
false
}
fn precompute_block_continuation_context(&self, lines: &[&str]) -> Vec<bool> {
let mut in_continuation_context = vec![false; lines.len()];
let mut last_list_item_line: Option<usize> = None;
let mut last_footnote_line: Option<usize> = None;
let mut blank_line_count = 0;
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim_start();
let indent_len = line.len() - trimmed.len();
if self.is_list_item(line) {
last_list_item_line = Some(i);
last_footnote_line = None; blank_line_count = 0;
in_continuation_context[i] = true;
continue;
}
if self.is_footnote_definition(line) {
last_footnote_line = Some(i);
last_list_item_line = None; blank_line_count = 0;
in_continuation_context[i] = true;
continue;
}
if line.trim().is_empty() {
if last_list_item_line.is_some() || last_footnote_line.is_some() {
blank_line_count += 1;
in_continuation_context[i] = true;
}
continue;
}
if indent_len == 0 && !trimmed.is_empty() {
if trimmed.starts_with('#') {
last_list_item_line = None;
last_footnote_line = None;
blank_line_count = 0;
continue;
}
if trimmed.starts_with("---") || trimmed.starts_with("***") {
last_list_item_line = None;
last_footnote_line = None;
blank_line_count = 0;
continue;
}
if let Some(list_line) = last_list_item_line
&& (i - list_line > 5 || blank_line_count > 1)
{
last_list_item_line = None;
}
if last_footnote_line.is_some() {
last_footnote_line = None;
}
blank_line_count = 0;
if last_list_item_line.is_none() && last_footnote_line.is_some() {
last_footnote_line = None;
}
continue;
}
if indent_len > 0 && (last_list_item_line.is_some() || last_footnote_line.is_some()) {
in_continuation_context[i] = true;
blank_line_count = 0;
}
}
in_continuation_context
}
fn is_indented_code_block_with_context(
&self,
lines: &[&str],
i: usize,
is_mkdocs: bool,
ctx: &IndentContext,
) -> bool {
if i >= lines.len() {
return false;
}
let line = lines[i];
let indent = calculate_indentation_width_default(line);
if indent < 4 {
return false;
}
if ctx.in_list_context[i] {
return false;
}
if is_mkdocs && ctx.in_tab_context[i] {
return false;
}
if is_mkdocs && ctx.in_admonition_context[i] {
return false;
}
if ctx.in_jsx_context.get(i).copied().unwrap_or(false) {
return false;
}
let has_blank_line_before = i == 0 || lines[i - 1].trim().is_empty();
let prev_is_indented_code = i > 0
&& calculate_indentation_width_default(lines[i - 1]) >= 4
&& !ctx.in_list_context[i - 1]
&& !(is_mkdocs && ctx.in_tab_context[i - 1])
&& !(is_mkdocs && ctx.in_admonition_context[i - 1]);
if !has_blank_line_before && !prev_is_indented_code {
return false;
}
true
}
fn precompute_mkdocs_tab_context(&self, lines: &[&str]) -> Vec<bool> {
let mut in_tab_context = vec![false; lines.len()];
let mut current_tab_indent: Option<usize> = None;
for (i, line) in lines.iter().enumerate() {
if mkdocs_tabs::is_tab_marker(line) {
let tab_indent = mkdocs_tabs::get_tab_indent(line).unwrap_or(0);
current_tab_indent = Some(tab_indent);
in_tab_context[i] = true;
continue;
}
if let Some(tab_indent) = current_tab_indent {
if mkdocs_tabs::is_tab_content(line, tab_indent) {
in_tab_context[i] = true;
} else if !line.trim().is_empty() && calculate_indentation_width_default(line) < 4 {
current_tab_indent = None;
} else {
in_tab_context[i] = true;
}
}
}
in_tab_context
}
fn precompute_mkdocs_admonition_context(&self, lines: &[&str]) -> Vec<bool> {
let mut in_admonition_context = vec![false; lines.len()];
let mut admonition_stack: Vec<usize> = Vec::new();
for (i, line) in lines.iter().enumerate() {
let line_indent = calculate_indentation_width_default(line);
if mkdocs_admonitions::is_admonition_start(line) {
let adm_indent = mkdocs_admonitions::get_admonition_indent(line).unwrap_or(0);
while let Some(&top_indent) = admonition_stack.last() {
if adm_indent <= top_indent {
admonition_stack.pop();
} else {
break;
}
}
admonition_stack.push(adm_indent);
in_admonition_context[i] = true;
continue;
}
if line.trim().is_empty() {
if !admonition_stack.is_empty() {
in_admonition_context[i] = true;
}
continue;
}
while let Some(&top_indent) = admonition_stack.last() {
if line_indent >= top_indent + 4 {
break;
} else {
admonition_stack.pop();
}
}
if !admonition_stack.is_empty() {
in_admonition_context[i] = true;
}
}
in_admonition_context
}
fn categorize_indented_blocks(
&self,
lines: &[&str],
is_mkdocs: bool,
in_list_context: &[bool],
in_tab_context: &[bool],
in_admonition_context: &[bool],
in_jsx_context: &[bool],
) -> (Vec<bool>, Vec<bool>) {
let mut is_misplaced = vec![false; lines.len()];
let mut contains_fences = vec![false; lines.len()];
let ictx = IndentContext {
in_list_context,
in_tab_context,
in_admonition_context,
in_jsx_context,
};
let mut i = 0;
while i < lines.len() {
if !self.is_indented_code_block_with_context(lines, i, is_mkdocs, &ictx) {
i += 1;
continue;
}
let block_start = i;
let mut block_end = i;
while block_end < lines.len()
&& self.is_indented_code_block_with_context(lines, block_end, is_mkdocs, &ictx)
{
block_end += 1;
}
if block_end > block_start {
let first_line = lines[block_start].trim_start();
let last_line = lines[block_end - 1].trim_start();
let is_backtick_fence = first_line.starts_with("```");
let is_tilde_fence = first_line.starts_with("~~~");
if is_backtick_fence || is_tilde_fence {
let fence_char = if is_backtick_fence { '`' } else { '~' };
let opener_len = first_line.chars().take_while(|&c| c == fence_char).count();
let closer_fence_len = last_line.chars().take_while(|&c| c == fence_char).count();
let after_closer = &last_line[closer_fence_len..];
if closer_fence_len >= opener_len && after_closer.trim().is_empty() {
is_misplaced[block_start..block_end].fill(true);
} else {
contains_fences[block_start..block_end].fill(true);
}
} else {
let has_fence_markers = (block_start..block_end).any(|j| {
let trimmed = lines[j].trim_start();
trimmed.starts_with("```") || trimmed.starts_with("~~~")
});
if has_fence_markers {
contains_fences[block_start..block_end].fill(true);
}
}
}
i = block_end;
}
(is_misplaced, contains_fences)
}
fn check_unclosed_code_blocks(
&self,
ctx: &crate::lint_context::LintContext,
) -> Result<Vec<LintWarning>, LintError> {
let mut warnings = Vec::new();
let lines = ctx.raw_lines();
let has_markdown_doc_block = ctx.code_block_details.iter().any(|d| {
if !d.is_fenced {
return false;
}
let lang = d.info_string.to_lowercase();
lang.starts_with("markdown") || lang.starts_with("md")
});
if has_markdown_doc_block {
return Ok(warnings);
}
for detail in &ctx.code_block_details {
if !detail.is_fenced {
continue;
}
if detail.end != ctx.content.len() {
continue;
}
let opening_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
Ok(idx) => idx,
Err(idx) => idx.saturating_sub(1),
};
let line = lines.get(opening_line_idx).unwrap_or(&"");
let trimmed = line.trim();
let fence_marker = if let Some(pos) = trimmed.find("```") {
let count = trimmed[pos..].chars().take_while(|&c| c == '`').count();
"`".repeat(count)
} else if let Some(pos) = trimmed.find("~~~") {
let count = trimmed[pos..].chars().take_while(|&c| c == '~').count();
"~".repeat(count)
} else {
"```".to_string()
};
let last_non_empty_line = lines.iter().rev().find(|l| !l.trim().is_empty()).unwrap_or(&"");
let last_trimmed = last_non_empty_line.trim();
let fence_char = fence_marker.chars().next().unwrap_or('`');
let has_closing_fence = if fence_char == '`' {
last_trimmed.starts_with("```") && {
let fence_len = last_trimmed.chars().take_while(|&c| c == '`').count();
last_trimmed[fence_len..].trim().is_empty()
}
} else {
last_trimmed.starts_with("~~~") && {
let fence_len = last_trimmed.chars().take_while(|&c| c == '~').count();
last_trimmed[fence_len..].trim().is_empty()
}
};
if !has_closing_fence {
if ctx.lines.get(opening_line_idx).is_some_and(|info| info.in_html_comment) {
continue;
}
let (start_line, start_col, end_line, end_col) = calculate_line_range(opening_line_idx + 1, line);
warnings.push(LintWarning {
rule_name: Some(self.name().to_string()),
line: start_line,
column: start_col,
end_line,
end_column: end_col,
message: format!("Code block opened with '{fence_marker}' but never closed"),
severity: Severity::Warning,
fix: Some(Fix {
range: (ctx.content.len()..ctx.content.len()),
replacement: format!("\n{fence_marker}"),
}),
});
}
}
Ok(warnings)
}
fn detect_style(&self, lines: &[&str], is_mkdocs: bool, ictx: &IndentContext) -> Option<CodeBlockStyle> {
if lines.is_empty() {
return None;
}
let mut fenced_count = 0;
let mut indented_count = 0;
let mut in_fenced = false;
let mut prev_was_indented = false;
for (i, line) in lines.iter().enumerate() {
if self.is_fenced_code_block_start(line) {
if !in_fenced {
fenced_count += 1;
in_fenced = true;
} else {
in_fenced = false;
}
} else if !in_fenced && self.is_indented_code_block_with_context(lines, i, is_mkdocs, ictx) {
if !prev_was_indented {
indented_count += 1;
}
prev_was_indented = true;
} else {
prev_was_indented = false;
}
}
if fenced_count == 0 && indented_count == 0 {
None
} else if fenced_count > 0 && indented_count == 0 {
Some(CodeBlockStyle::Fenced)
} else if fenced_count == 0 && indented_count > 0 {
Some(CodeBlockStyle::Indented)
} else if fenced_count >= indented_count {
Some(CodeBlockStyle::Fenced)
} else {
Some(CodeBlockStyle::Indented)
}
}
}
impl Rule for MD046CodeBlockStyle {
fn name(&self) -> &'static str {
"MD046"
}
fn description(&self) -> &'static str {
"Code blocks should use a consistent style"
}
fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
if ctx.content.is_empty() {
return Ok(Vec::new());
}
if !ctx.content.contains("```")
&& !ctx.content.contains("~~~")
&& !ctx.content.contains(" ")
&& !ctx.content.contains('\t')
{
return Ok(Vec::new());
}
let unclosed_warnings = self.check_unclosed_code_blocks(ctx)?;
if !unclosed_warnings.is_empty() {
return Ok(unclosed_warnings);
}
let lines = ctx.raw_lines();
let mut warnings = Vec::new();
let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
let target_style = match self.config.style {
CodeBlockStyle::Consistent => {
let in_list_context = self.precompute_block_continuation_context(lines);
let in_jsx_context: Vec<bool> = (0..lines.len())
.map(|i| ctx.line_info(i + 1).is_some_and(|info| info.in_jsx_block))
.collect();
let in_tab_context = if is_mkdocs {
self.precompute_mkdocs_tab_context(lines)
} else {
vec![false; lines.len()]
};
let in_admonition_context = if is_mkdocs {
self.precompute_mkdocs_admonition_context(lines)
} else {
vec![false; lines.len()]
};
let ictx = IndentContext {
in_list_context: &in_list_context,
in_tab_context: &in_tab_context,
in_admonition_context: &in_admonition_context,
in_jsx_context: &in_jsx_context,
};
self.detect_style(lines, is_mkdocs, &ictx)
.unwrap_or(CodeBlockStyle::Fenced)
}
_ => self.config.style,
};
let footnote_ranges =
if target_style == CodeBlockStyle::Fenced && ctx.code_block_details.iter().any(|d| !d.is_fenced) {
compute_footnote_ranges(ctx.content)
} else {
Vec::new()
};
let mut reported_indented_lines: std::collections::HashSet<usize> = std::collections::HashSet::new();
for detail in &ctx.code_block_details {
if detail.start >= ctx.content.len() || detail.end > ctx.content.len() {
continue;
}
let start_line_idx = match ctx.line_offsets.binary_search(&detail.start) {
Ok(idx) => idx,
Err(idx) => idx.saturating_sub(1),
};
if detail.is_fenced {
if target_style == CodeBlockStyle::Indented {
let line = lines.get(start_line_idx).unwrap_or(&"");
if ctx.lines.get(start_line_idx).is_some_and(|info| info.in_html_comment) {
continue;
}
let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
warnings.push(LintWarning {
rule_name: Some(self.name().to_string()),
line: start_line,
column: start_col,
end_line,
end_column: end_col,
message: "Use indented code blocks".to_string(),
severity: Severity::Warning,
fix: None,
});
}
} else {
if target_style == CodeBlockStyle::Fenced && !reported_indented_lines.contains(&start_line_idx) {
let line = lines.get(start_line_idx).unwrap_or(&"");
if ctx.lines.get(start_line_idx).is_some_and(|info| {
info.in_html_comment
|| info.in_html_block
|| info.in_jsx_block
|| info.in_mkdocstrings
|| info.blockquote.is_some()
}) {
continue;
}
if is_in_footnote_range(&footnote_ranges, detail.start) {
continue;
}
if is_mkdocs
&& ctx
.lines
.get(start_line_idx)
.is_some_and(|info| info.in_admonition || info.in_content_tab)
{
continue;
}
reported_indented_lines.insert(start_line_idx);
let (start_line, start_col, end_line, end_col) = calculate_line_range(start_line_idx + 1, line);
warnings.push(LintWarning {
rule_name: Some(self.name().to_string()),
line: start_line,
column: start_col,
end_line,
end_column: end_col,
message: "Use fenced code blocks".to_string(),
severity: Severity::Warning,
fix: None,
});
}
}
}
warnings.sort_by_key(|w| (w.line, w.column));
Ok(warnings)
}
fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
let content = ctx.content;
if content.is_empty() {
return Ok(String::new());
}
let lines = ctx.raw_lines();
let is_mkdocs = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
let in_jsx_context: Vec<bool> = (0..lines.len())
.map(|i| ctx.line_info(i + 1).is_some_and(|info| info.in_jsx_block))
.collect();
let in_list_context = self.precompute_block_continuation_context(lines);
let in_tab_context = if is_mkdocs {
self.precompute_mkdocs_tab_context(lines)
} else {
vec![false; lines.len()]
};
let in_admonition_context = if is_mkdocs {
self.precompute_mkdocs_admonition_context(lines)
} else {
vec![false; lines.len()]
};
let target_style = match self.config.style {
CodeBlockStyle::Consistent => {
let ictx = IndentContext {
in_list_context: &in_list_context,
in_tab_context: &in_tab_context,
in_admonition_context: &in_admonition_context,
in_jsx_context: &in_jsx_context,
};
self.detect_style(lines, is_mkdocs, &ictx)
.unwrap_or(CodeBlockStyle::Fenced)
}
_ => self.config.style,
};
let (misplaced_fence_lines, unsafe_fence_lines) = self.categorize_indented_blocks(
lines,
is_mkdocs,
&in_list_context,
&in_tab_context,
&in_admonition_context,
&in_jsx_context,
);
let ictx = IndentContext {
in_list_context: &in_list_context,
in_tab_context: &in_tab_context,
in_admonition_context: &in_admonition_context,
in_jsx_context: &in_jsx_context,
};
let mut result = String::with_capacity(content.len());
let mut in_fenced_block = false;
let mut fenced_fence_type = None;
let mut in_indented_block = false;
let mut current_block_disabled = false;
for (i, line) in lines.iter().enumerate() {
let line_num = i + 1;
let trimmed = line.trim_start();
if !in_fenced_block
&& Self::has_valid_fence_indent(line)
&& (trimmed.starts_with("```") || trimmed.starts_with("~~~"))
{
current_block_disabled = ctx.inline_config().is_rule_disabled(self.name(), line_num);
in_fenced_block = true;
fenced_fence_type = Some(if trimmed.starts_with("```") { "```" } else { "~~~" });
if current_block_disabled {
result.push_str(line);
result.push('\n');
} else if target_style == CodeBlockStyle::Indented {
in_indented_block = true;
} else {
result.push_str(line);
result.push('\n');
}
} else if in_fenced_block && fenced_fence_type.is_some() {
let fence = fenced_fence_type.unwrap();
if trimmed.starts_with(fence) {
in_fenced_block = false;
fenced_fence_type = None;
in_indented_block = false;
if current_block_disabled {
result.push_str(line);
result.push('\n');
} else if target_style == CodeBlockStyle::Indented {
} else {
result.push_str(line);
result.push('\n');
}
current_block_disabled = false;
} else if current_block_disabled {
result.push_str(line);
result.push('\n');
} else if target_style == CodeBlockStyle::Indented {
result.push_str(" ");
result.push_str(line);
result.push('\n');
} else {
result.push_str(line);
result.push('\n');
}
} else if self.is_indented_code_block_with_context(lines, i, is_mkdocs, &ictx) {
if ctx.inline_config().is_rule_disabled(self.name(), line_num) {
result.push_str(line);
result.push('\n');
continue;
}
let prev_line_is_indented =
i > 0 && self.is_indented_code_block_with_context(lines, i - 1, is_mkdocs, &ictx);
if target_style == CodeBlockStyle::Fenced {
let trimmed_content = line.trim_start();
if misplaced_fence_lines[i] {
result.push_str(trimmed_content);
result.push('\n');
} else if unsafe_fence_lines[i] {
result.push_str(line);
result.push('\n');
} else if !prev_line_is_indented && !in_indented_block {
result.push_str("```\n");
result.push_str(trimmed_content);
result.push('\n');
in_indented_block = true;
} else {
result.push_str(trimmed_content);
result.push('\n');
}
let next_line_is_indented =
i < lines.len() - 1 && self.is_indented_code_block_with_context(lines, i + 1, is_mkdocs, &ictx);
if !next_line_is_indented
&& in_indented_block
&& !misplaced_fence_lines[i]
&& !unsafe_fence_lines[i]
{
result.push_str("```\n");
in_indented_block = false;
}
} else {
result.push_str(line);
result.push('\n');
}
} else {
if in_indented_block && target_style == CodeBlockStyle::Fenced {
result.push_str("```\n");
in_indented_block = false;
}
result.push_str(line);
result.push('\n');
}
}
if in_indented_block && target_style == CodeBlockStyle::Fenced {
result.push_str("```\n");
}
if let Some(fence_type) = fenced_fence_type
&& in_fenced_block
{
result.push_str(fence_type);
result.push('\n');
}
if !content.ends_with('\n') && result.ends_with('\n') {
result.pop();
}
Ok(result)
}
fn category(&self) -> RuleCategory {
RuleCategory::CodeBlock
}
fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~') && !ctx.content.contains(" "))
}
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::<MD046Config>(config);
Box::new(Self::from_config_struct(rule_config))
}
}
fn compute_footnote_ranges(content: &str) -> Vec<(usize, usize)> {
let mut ranges = Vec::new();
let mut footnote_start: Option<(usize, usize)> = None;
let mut offset = 0;
for line in content.split('\n') {
let line_end = offset + line.len();
if mkdocs_footnotes::is_footnote_definition(line) {
if let Some((start, _)) = footnote_start.take() {
ranges.push((start, offset.saturating_sub(1)));
}
let indent = mkdocs_footnotes::get_footnote_indent(line).unwrap_or(0);
footnote_start = Some((offset, indent));
} else if let Some((_, indent)) = footnote_start
&& !line.trim().is_empty()
&& !mkdocs_footnotes::is_footnote_continuation(line, indent)
{
let (start, _) = footnote_start.take().unwrap();
ranges.push((start, offset.saturating_sub(1)));
}
offset = line_end + 1; }
if let Some((start, _)) = footnote_start {
ranges.push((start, content.len()));
}
ranges
}
fn is_in_footnote_range(ranges: &[(usize, usize)], pos: usize) -> bool {
let idx = ranges.partition_point(|&(start, _)| start <= pos);
idx > 0 && pos < ranges[idx - 1].1
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lint_context::LintContext;
fn detect_style_from_content(
rule: &MD046CodeBlockStyle,
content: &str,
is_mkdocs: bool,
in_jsx_context: &[bool],
) -> Option<CodeBlockStyle> {
let lines: Vec<&str> = content.lines().collect();
let in_list_context = rule.precompute_block_continuation_context(&lines);
let in_tab_context = if is_mkdocs {
rule.precompute_mkdocs_tab_context(&lines)
} else {
vec![false; lines.len()]
};
let in_admonition_context = if is_mkdocs {
rule.precompute_mkdocs_admonition_context(&lines)
} else {
vec![false; lines.len()]
};
let ictx = IndentContext {
in_list_context: &in_list_context,
in_tab_context: &in_tab_context,
in_admonition_context: &in_admonition_context,
in_jsx_context,
};
rule.detect_style(&lines, is_mkdocs, &ictx)
}
#[test]
fn test_fenced_code_block_detection() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
assert!(rule.is_fenced_code_block_start("```"));
assert!(rule.is_fenced_code_block_start("```rust"));
assert!(rule.is_fenced_code_block_start("~~~"));
assert!(rule.is_fenced_code_block_start("~~~python"));
assert!(rule.is_fenced_code_block_start(" ```"));
assert!(!rule.is_fenced_code_block_start("``"));
assert!(!rule.is_fenced_code_block_start("~~"));
assert!(!rule.is_fenced_code_block_start("Regular text"));
}
#[test]
fn test_consistent_style_with_fenced_blocks() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
let content = "```\ncode\n```\n\nMore text\n\n```\nmore code\n```";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_consistent_style_with_indented_blocks() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
let content = "Text\n\n code\n more code\n\nMore text\n\n another block";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_consistent_style_mixed() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
let content = "```\nfenced code\n```\n\nText\n\n indented code\n\nMore";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_fenced_style_with_indented_blocks() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = "Text\n\n indented code\n more code\n\nMore text";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty());
assert!(result[0].message.contains("Use fenced code blocks"));
}
#[test]
fn test_fenced_style_with_tab_indented_blocks() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = "Text\n\n\ttab indented code\n\tmore code\n\nMore text";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty());
assert!(result[0].message.contains("Use fenced code blocks"));
}
#[test]
fn test_fenced_style_with_mixed_whitespace_indented_blocks() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = "Text\n\n \tmixed indent code\n \tmore code\n\nMore text";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Mixed whitespace (2 spaces + tab) should be detected as indented code"
);
assert!(result[0].message.contains("Use fenced code blocks"));
}
#[test]
fn test_fenced_style_with_one_space_tab_indent() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = "Text\n\n \ttab after one space\n \tmore code\n\nMore text";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "1 space + tab should be detected as indented code");
assert!(result[0].message.contains("Use fenced code blocks"));
}
#[test]
fn test_indented_style_with_fenced_blocks() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
let content = "Text\n\n```\nfenced code\n```\n\nMore text";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty());
assert!(result[0].message.contains("Use indented code blocks"));
}
#[test]
fn test_unclosed_code_block() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = "```\ncode without closing fence";
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("never closed"));
}
#[test]
fn test_nested_code_blocks() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = "```\nouter\n```\n\ninner text\n\n```\ncode\n```";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_fix_indented_to_fenced() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = "Text\n\n code line 1\n code line 2\n\nMore text";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("```\ncode line 1\ncode line 2\n```"));
}
#[test]
fn test_fix_fenced_to_indented() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
let content = "Text\n\n```\ncode line 1\ncode line 2\n```\n\nMore text";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains(" code line 1\n code line 2"));
assert!(!fixed.contains("```"));
}
#[test]
fn test_fix_fenced_to_indented_preserves_internal_indentation() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
let content = r#"# Test
```html
<!doctype html>
<html>
<head>
<title>Test</title>
</head>
</html>
```
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains(" <head>"),
"Expected 6 spaces before <head> (4 for code block + 2 original), got:\n{fixed}"
);
assert!(
fixed.contains(" <title>"),
"Expected 8 spaces before <title> (4 for code block + 4 original), got:\n{fixed}"
);
assert!(!fixed.contains("```"), "Fenced markers should be removed");
}
#[test]
fn test_fix_fenced_to_indented_preserves_python_indentation() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
let content = r#"# Python Example
```python
def greet(name):
if name:
print(f"Hello, {name}!")
else:
print("Hello, World!")
```
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains(" def greet(name):"),
"Function def should have 4 spaces (code block indent)"
);
assert!(
fixed.contains(" if name:"),
"if statement should have 8 spaces (4 code + 4 Python)"
);
assert!(
fixed.contains(" print"),
"print should have 12 spaces (4 code + 8 Python)"
);
}
#[test]
fn test_fix_fenced_to_indented_preserves_yaml_indentation() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
let content = r#"# Config
```yaml
server:
host: localhost
port: 8080
ssl:
enabled: true
cert: /path/to/cert
```
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains(" server:"), "Root key should have 4 spaces");
assert!(fixed.contains(" host:"), "First level should have 6 spaces");
assert!(fixed.contains(" ssl:"), "ssl key should have 6 spaces");
assert!(fixed.contains(" enabled:"), "Nested ssl should have 8 spaces");
}
#[test]
fn test_fix_fenced_to_indented_preserves_empty_lines() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
let content = "```\nline1\n\nline2\n```\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains(" line1"), "line1 should be indented");
assert!(fixed.contains(" line2"), "line2 should be indented");
}
#[test]
fn test_fix_fenced_to_indented_multiple_blocks() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
let content = r#"# Doc
```python
def foo():
pass
```
Text between.
```yaml
key:
value: 1
```
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains(" def foo():"), "Python def should be indented");
assert!(fixed.contains(" pass"), "Python body should have 8 spaces");
assert!(fixed.contains(" key:"), "YAML root should have 4 spaces");
assert!(fixed.contains(" value:"), "YAML nested should have 6 spaces");
assert!(!fixed.contains("```"), "No fence markers should remain");
}
#[test]
fn test_fix_unclosed_block() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = "```\ncode without closing";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.ends_with("```"));
}
#[test]
fn test_code_block_in_list() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = "- List item\n code in list\n more code\n- Next item";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_detect_style_fenced() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
let content = "```\ncode\n```";
let style = detect_style_from_content(&rule, content, false, &[]);
assert_eq!(style, Some(CodeBlockStyle::Fenced));
}
#[test]
fn test_detect_style_indented() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
let content = "Text\n\n code\n\nMore";
let style = detect_style_from_content(&rule, content, false, &[]);
assert_eq!(style, Some(CodeBlockStyle::Indented));
}
#[test]
fn test_detect_style_none() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
let content = "No code blocks here";
let style = detect_style_from_content(&rule, content, false, &[]);
assert_eq!(style, None);
}
#[test]
fn test_tilde_fence() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = "~~~\ncode\n~~~";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_language_specification() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = "```rust\nfn main() {}\n```";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_empty_content() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
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 = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
let (name, _config) = rule.default_config_section().unwrap();
assert_eq!(name, "MD046");
}
#[test]
fn test_markdown_documentation_block() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = "```markdown\n# Example\n\n```\ncode\n```\n\nText\n```";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_preserve_trailing_newline() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = "```\ncode\n```\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, content);
}
#[test]
fn test_mkdocs_tabs_not_flagged_as_indented_code() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"# Document
=== "Python"
This is tab content
Not an indented code block
```python
def hello():
print("Hello")
```
=== "JavaScript"
More tab content here
Also not an indented code block"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_mkdocs_tabs_with_actual_indented_code() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"# Document
=== "Tab 1"
This is tab content
Regular text
This is an actual indented code block
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("Use fenced code blocks"));
}
#[test]
fn test_mkdocs_tabs_detect_style() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Consistent);
let content = r#"=== "Tab 1"
Content in tab
More content
=== "Tab 2"
Content in second tab"#;
let style = detect_style_from_content(&rule, content, true, &[]);
assert_eq!(style, None);
let style = detect_style_from_content(&rule, content, false, &[]);
assert_eq!(style, Some(CodeBlockStyle::Indented));
}
#[test]
fn test_mkdocs_nested_tabs() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"# Document
=== "Outer Tab"
Some content
=== "Nested Tab"
Nested tab content
Should not be flagged"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_mkdocs_admonitions_not_flagged_as_indented_code() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"# Document
!!! note
This is normal admonition content, not a code block.
It spans multiple lines.
??? warning "Collapsible Warning"
This is also admonition content.
???+ tip "Expanded Tip"
And this one too.
Regular text outside admonitions."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Admonition content in MkDocs mode should not trigger MD046"
);
}
#[test]
fn test_mkdocs_admonition_with_actual_indented_code() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"# Document
!!! note
This is admonition content.
Regular text ends the admonition.
This is actual indented code (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("Use fenced code blocks"));
}
#[test]
fn test_admonition_in_standard_mode_flagged() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"# Document
!!! note
This looks like code in standard mode.
Regular text."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Admonition content in Standard mode should be flagged as indented code"
);
}
#[test]
fn test_mkdocs_admonition_with_fenced_code_inside() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"# Document
!!! note "Code Example"
Here's some code:
```python
def hello():
print("world")
```
More text after code.
Regular text."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0, "Fenced code blocks inside admonitions should be valid");
}
#[test]
fn test_mkdocs_nested_admonitions() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"# Document
!!! note "Outer"
Outer content.
!!! warning "Inner"
Inner content.
More inner content.
Back to outer.
Regular text."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0, "Nested admonitions should not be flagged");
}
#[test]
fn test_mkdocs_admonition_fix_does_not_wrap() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"!!! note
Content that should stay as admonition content.
Not be wrapped in code fences.
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
!fixed.contains("```\n Content"),
"Admonition content should not be wrapped in fences"
);
assert_eq!(fixed, content, "Content should remain unchanged");
}
#[test]
fn test_mkdocs_empty_admonition() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"!!! note
Regular paragraph after empty admonition.
This IS an indented code block (after blank + non-indented line)."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Indented code after admonition ends should be flagged");
}
#[test]
fn test_mkdocs_indented_admonition() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"- List item
!!! note
Indented admonition content.
More content.
- Next item"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Indented admonitions (e.g., in lists) should not be flagged"
);
}
#[test]
fn test_footnote_indented_paragraphs_not_flagged() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"# Test Document with Footnotes
This is some text with a footnote[^1].
Here's some code:
```bash
echo "fenced code block"
```
More text with another footnote[^2].
[^1]: Really interesting footnote text.
Even more interesting second paragraph.
[^2]: Another footnote.
With a second paragraph too.
And even a third paragraph!"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_footnote_definition_detection() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
assert!(rule.is_footnote_definition("[^1]: Footnote text"));
assert!(rule.is_footnote_definition("[^foo]: Footnote text"));
assert!(rule.is_footnote_definition("[^long-name]: Footnote text"));
assert!(rule.is_footnote_definition("[^test_123]: Mixed chars"));
assert!(rule.is_footnote_definition(" [^1]: Indented footnote"));
assert!(rule.is_footnote_definition("[^a]: Minimal valid footnote"));
assert!(rule.is_footnote_definition("[^123]: Numeric label"));
assert!(rule.is_footnote_definition("[^_]: Single underscore"));
assert!(rule.is_footnote_definition("[^-]: Single hyphen"));
assert!(!rule.is_footnote_definition("[^]: No label"));
assert!(!rule.is_footnote_definition("[^ ]: Whitespace only"));
assert!(!rule.is_footnote_definition("[^ ]: Multiple spaces"));
assert!(!rule.is_footnote_definition("[^\t]: Tab only"));
assert!(!rule.is_footnote_definition("[^]]: Extra bracket"));
assert!(!rule.is_footnote_definition("Regular text [^1]:"));
assert!(!rule.is_footnote_definition("[1]: Not a footnote"));
assert!(!rule.is_footnote_definition("[^")); assert!(!rule.is_footnote_definition("[^1:")); assert!(!rule.is_footnote_definition("^1]: Missing opening bracket"));
assert!(!rule.is_footnote_definition("[^test.name]: Period"));
assert!(!rule.is_footnote_definition("[^test name]: Space in label"));
assert!(!rule.is_footnote_definition("[^test@name]: Special char"));
assert!(!rule.is_footnote_definition("[^test/name]: Slash"));
assert!(!rule.is_footnote_definition("[^test\\name]: Backslash"));
assert!(!rule.is_footnote_definition("[^test\r]: Carriage return"));
}
#[test]
fn test_footnote_with_blank_lines() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"# Document
Text with footnote[^1].
[^1]: First paragraph.
Second paragraph after blank line.
Third paragraph after another blank line.
Regular text at column 0 ends the footnote."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Indented content within footnotes should not trigger MD046"
);
}
#[test]
fn test_footnote_multiple_consecutive_blank_lines() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"Text[^1].
[^1]: First paragraph.
Content after three blank lines (still part of footnote).
Not indented, so footnote ends here."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Multiple blank lines shouldn't break footnote continuation"
);
}
#[test]
fn test_footnote_terminated_by_non_indented_content() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"[^1]: Footnote content.
More indented content in footnote.
This paragraph is not indented, so footnote ends.
This should be flagged as indented code block."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Indented code after footnote termination should be flagged"
);
assert!(
result[0].message.contains("Use fenced code blocks"),
"Expected MD046 warning for indented code block"
);
assert!(result[0].line >= 7, "Warning should be on the indented code block line");
}
#[test]
fn test_footnote_terminated_by_structural_elements() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"[^1]: Footnote content.
More content.
## Heading terminates footnote
This indented content should be flagged.
---
This should also be flagged (after horizontal rule)."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
2,
"Both indented blocks after termination should be flagged"
);
}
#[test]
fn test_footnote_with_code_block_inside() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"Text[^1].
[^1]: Footnote with code:
```python
def hello():
print("world")
```
More footnote text after code."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0, "Fenced code blocks within footnotes should be allowed");
}
#[test]
fn test_footnote_with_8_space_indented_code() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"Text[^1].
[^1]: Footnote with nested code.
code block
more code"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"8-space indented code within footnotes represents nested code blocks"
);
}
#[test]
fn test_multiple_footnotes() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"Text[^1] and more[^2].
[^1]: First footnote.
Continuation of first.
[^2]: Second footnote starts here, ending the first.
Continuation of second."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Multiple footnotes should each maintain their continuation context"
);
}
#[test]
fn test_list_item_ends_footnote_context() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"[^1]: Footnote.
Content in footnote.
- List item starts here (ends footnote context).
This indented content is part of the list, not the footnote."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"List items should end footnote context and start their own"
);
}
#[test]
fn test_footnote_vs_actual_indented_code() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"# Heading
Text with footnote[^1].
[^1]: Footnote content.
Part of footnote (should not be flagged).
Regular paragraph ends footnote context.
This is actual indented code (MUST be flagged)
Should be detected as code block"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Must still detect indented code blocks outside footnotes"
);
assert!(
result[0].message.contains("Use fenced code blocks"),
"Expected MD046 warning for indented code"
);
assert!(
result[0].line >= 11,
"Warning should be on the actual indented code line"
);
}
#[test]
fn test_spec_compliant_label_characters() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
assert!(rule.is_footnote_definition("[^test]: text"));
assert!(rule.is_footnote_definition("[^TEST]: text"));
assert!(rule.is_footnote_definition("[^test-name]: text"));
assert!(rule.is_footnote_definition("[^test_name]: text"));
assert!(rule.is_footnote_definition("[^test123]: text"));
assert!(rule.is_footnote_definition("[^123]: text"));
assert!(rule.is_footnote_definition("[^a1b2c3]: text"));
assert!(!rule.is_footnote_definition("[^test.name]: text")); assert!(!rule.is_footnote_definition("[^test name]: text")); assert!(!rule.is_footnote_definition("[^test@name]: text")); assert!(!rule.is_footnote_definition("[^test#name]: text")); assert!(!rule.is_footnote_definition("[^test$name]: text")); assert!(!rule.is_footnote_definition("[^test%name]: text")); }
#[test]
fn test_code_block_inside_html_comment() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"# Document
Some text.
<!--
Example code block in comment:
```typescript
console.log("Hello");
```
More comment text.
-->
More content."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Code blocks inside HTML comments should not be flagged as unclosed"
);
}
#[test]
fn test_unclosed_fence_inside_html_comment() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"# Document
<!--
Example with intentionally unclosed fence:
```
code without closing
-->
More content."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Unclosed fences inside HTML comments should be ignored"
);
}
#[test]
fn test_multiline_html_comment_with_indented_code() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"# Document
<!--
Example:
indented code
more code
End of comment.
-->
Regular text."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"Indented code inside HTML comments should not be flagged"
);
}
#[test]
fn test_code_block_after_html_comment() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"# Document
<!-- comment -->
Text before.
indented code should be flagged
More text."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Code blocks after HTML comments should still be detected"
);
assert!(result[0].message.contains("Use fenced code blocks"));
}
#[test]
fn test_four_space_indented_fence_is_not_valid_fence() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
assert!(rule.is_fenced_code_block_start("```"));
assert!(rule.is_fenced_code_block_start(" ```"));
assert!(rule.is_fenced_code_block_start(" ```"));
assert!(rule.is_fenced_code_block_start(" ```"));
assert!(!rule.is_fenced_code_block_start(" ```"));
assert!(!rule.is_fenced_code_block_start(" ```"));
assert!(!rule.is_fenced_code_block_start(" ```"));
assert!(!rule.is_fenced_code_block_start("\t```"));
}
#[test]
fn test_issue_237_indented_fenced_block_detected_as_indented() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"## Test
```js
var foo = "hello";
```
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"4-space indented fence should be detected as indented code block"
);
assert!(
result[0].message.contains("Use fenced code blocks"),
"Expected 'Use fenced code blocks' message"
);
}
#[test]
fn test_issue_276_indented_code_in_list() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"1. First item
2. Second item with code:
# This is a code block in a list
print("Hello, world!")
4. Third item"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
!result.is_empty(),
"Indented code block inside list should be flagged when style=fenced"
);
assert!(
result[0].message.contains("Use fenced code blocks"),
"Expected 'Use fenced code blocks' message"
);
}
#[test]
fn test_three_space_indented_fence_is_valid() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"## Test
```js
var foo = "hello";
```
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"3-space indented fence should be recognized as valid fenced code block"
);
}
#[test]
fn test_indented_style_with_deeply_indented_fenced() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Indented);
let content = r#"Text
```js
var foo = "hello";
```
More text
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
0,
"4-space indented content should be valid when style=indented"
);
}
#[test]
fn test_fix_misplaced_fenced_block() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"## Test
```js
var foo = "hello";
```
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
let expected = r#"## Test
```js
var foo = "hello";
```
"#;
assert_eq!(fixed, expected, "Fix should remove indentation, not add more fences");
}
#[test]
fn test_fix_regular_indented_block() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"Text
var foo = "hello";
console.log(foo);
More text
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains("```\nvar foo"), "Should add opening fence");
assert!(fixed.contains("console.log(foo);\n```"), "Should add closing fence");
}
#[test]
fn test_fix_indented_block_with_fence_like_content() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"Text
some code
```not a fence opener
more code
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.contains(" some code"), "Unsafe block should be left unchanged");
assert!(!fixed.contains("```\nsome code"), "Should NOT wrap unsafe block");
}
#[test]
fn test_fix_mixed_indented_and_misplaced_blocks() {
let rule = MD046CodeBlockStyle::new(CodeBlockStyle::Fenced);
let content = r#"Text
regular indented code
More text
```python
print("hello")
```
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains("```\nregular indented code\n```"),
"First block should be wrapped in fences"
);
assert!(
fixed.contains("\n```python\nprint(\"hello\")\n```"),
"Second block should be dedented, not double-wrapped"
);
assert!(
!fixed.contains("```\n```python"),
"Should not have nested fence openers"
);
}
}