use crate::code_block_tools::{CodeBlockToolsConfig, RUMDL_BUILTIN_TOOL};
use crate::config as rumdl_config;
use crate::inline_config::InlineConfig;
use crate::lint_context::LintContext;
use crate::rule::{LintWarning, Rule};
use crate::utils::code_block_utils::CodeBlockUtils;
pub const MAX_EMBEDDED_DEPTH: usize = 5;
pub fn should_lint_embedded_markdown(config: &CodeBlockToolsConfig) -> bool {
if !config.enabled {
return false;
}
for lang_key in ["markdown", "md"] {
if let Some(lang_config) = config.languages.get(lang_key)
&& lang_config.enabled
&& lang_config.lint.iter().any(|tool| tool == RUMDL_BUILTIN_TOOL)
{
return true;
}
}
false
}
pub fn has_fenced_code_blocks(content: &str) -> bool {
content.contains("```") || content.contains("~~~")
}
pub fn check_embedded_markdown_blocks(
content: &str,
rules: &[Box<dyn Rule>],
config: &rumdl_config::Config,
) -> Vec<LintWarning> {
check_embedded_markdown_blocks_recursive(content, rules, config, 0)
}
fn check_embedded_markdown_blocks_recursive(
content: &str,
rules: &[Box<dyn Rule>],
config: &rumdl_config::Config,
depth: usize,
) -> Vec<LintWarning> {
if depth >= MAX_EMBEDDED_DEPTH {
return Vec::new();
}
if !has_fenced_code_blocks(content) {
return Vec::new();
}
let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
if blocks.is_empty() {
return Vec::new();
}
let inline_config = InlineConfig::from_content(content);
let mut all_warnings = Vec::new();
for block in blocks {
let block_content = &content[block.content_start..block.content_end];
if block_content.trim().is_empty() {
continue;
}
let line_offset = content[..block.content_start].matches('\n').count();
let block_line = line_offset + 1;
let block_rules: Vec<&Box<dyn Rule>> = rules
.iter()
.filter(|rule| !inline_config.is_rule_disabled(rule.name(), block_line))
.collect();
let (stripped_content, _common_indent) = strip_common_indent(block_content);
let block_rules_owned: Vec<Box<dyn Rule>> = block_rules.iter().map(|r| dyn_clone::clone_box(&***r)).collect();
let nested_warnings =
check_embedded_markdown_blocks_recursive(&stripped_content, &block_rules_owned, config, depth + 1);
for mut warning in nested_warnings {
warning.line += line_offset;
warning.end_line += line_offset;
warning.fix = None;
all_warnings.push(warning);
}
let ctx = LintContext::new(&stripped_content, config.markdown_flavor(), None);
for rule in &block_rules {
match rule.name() {
"MD041" => continue, "MD047" => continue, _ => {}
}
if let Ok(rule_warnings) = rule.check(&ctx) {
for warning in rule_warnings {
let adjusted_warning = LintWarning {
message: warning.message.clone(),
line: warning.line + line_offset,
column: warning.column,
end_line: warning.end_line + line_offset,
end_column: warning.end_column,
severity: warning.severity,
fix: None,
rule_name: warning.rule_name,
};
all_warnings.push(adjusted_warning);
}
}
}
}
all_warnings
}
pub fn strip_common_indent(content: &str) -> (String, String) {
let lines: Vec<&str> = content.lines().collect();
let has_trailing_newline = content.ends_with('\n');
let min_indent = lines
.iter()
.filter(|line| !line.trim().is_empty())
.map(|line| line.len() - line.trim_start().len())
.min()
.unwrap_or(0);
let mut stripped: String = lines
.iter()
.map(|line| {
if line.trim().is_empty() {
""
} else if line.len() >= min_indent {
&line[min_indent..]
} else {
line.trim_start()
}
})
.collect::<Vec<_>>()
.join("\n");
if has_trailing_newline && !stripped.ends_with('\n') {
stripped.push('\n');
}
let indent_str = " ".repeat(min_indent);
(stripped, indent_str)
}