rumdl_lib/
embedded_lint.rs1use crate::code_block_tools::{CodeBlockToolsConfig, RUMDL_BUILTIN_TOOL};
8use crate::config as rumdl_config;
9use crate::inline_config::InlineConfig;
10use crate::lint_context::LintContext;
11use crate::rule::{LintWarning, Rule};
12use crate::utils::code_block_utils::CodeBlockUtils;
13
14pub const MAX_EMBEDDED_DEPTH: usize = 5;
19
20pub fn should_lint_embedded_markdown(config: &CodeBlockToolsConfig) -> bool {
25 if !config.enabled {
26 return false;
27 }
28
29 for lang_key in ["markdown", "md"] {
31 if let Some(lang_config) = config.languages.get(lang_key)
32 && lang_config.enabled
33 && lang_config.lint.iter().any(|tool| tool == RUMDL_BUILTIN_TOOL)
34 {
35 return true;
36 }
37 }
38
39 false
40}
41
42pub fn has_fenced_code_blocks(content: &str) -> bool {
44 content.contains("```") || content.contains("~~~")
45}
46
47pub fn check_embedded_markdown_blocks(
53 content: &str,
54 rules: &[Box<dyn Rule>],
55 config: &rumdl_config::Config,
56) -> Vec<LintWarning> {
57 check_embedded_markdown_blocks_recursive(content, rules, config, 0)
58}
59
60fn check_embedded_markdown_blocks_recursive(
62 content: &str,
63 rules: &[Box<dyn Rule>],
64 config: &rumdl_config::Config,
65 depth: usize,
66) -> Vec<LintWarning> {
67 if depth >= MAX_EMBEDDED_DEPTH {
68 return Vec::new();
69 }
70 if !has_fenced_code_blocks(content) {
71 return Vec::new();
72 }
73
74 let blocks = CodeBlockUtils::detect_markdown_code_blocks(content);
75
76 if blocks.is_empty() {
77 return Vec::new();
78 }
79
80 let inline_config = InlineConfig::from_content(content);
81 let mut all_warnings = Vec::new();
82
83 for block in blocks {
84 let block_content = &content[block.content_start..block.content_end];
85
86 if block_content.trim().is_empty() {
87 continue;
88 }
89
90 let line_offset = content[..block.content_start].matches('\n').count();
92
93 let block_line = line_offset + 1;
95
96 let block_rules: Vec<&Box<dyn Rule>> = rules
98 .iter()
99 .filter(|rule| !inline_config.is_rule_disabled(rule.name(), block_line))
100 .collect();
101
102 let (stripped_content, _common_indent) = strip_common_indent(block_content);
103
104 let block_rules_owned: Vec<Box<dyn Rule>> = block_rules.iter().map(|r| dyn_clone::clone_box(&***r)).collect();
106 let nested_warnings =
107 check_embedded_markdown_blocks_recursive(&stripped_content, &block_rules_owned, config, depth + 1);
108
109 for mut warning in nested_warnings {
111 warning.line += line_offset;
112 warning.end_line += line_offset;
113 warning.fix = None;
114 all_warnings.push(warning);
115 }
116
117 let ctx = LintContext::new(&stripped_content, config.markdown_flavor(), None);
119 for rule in &block_rules {
120 match rule.name() {
121 "MD041" => continue, "MD047" => continue, _ => {}
124 }
125
126 if let Ok(rule_warnings) = rule.check(&ctx) {
127 for warning in rule_warnings {
128 let adjusted_warning = LintWarning {
129 message: warning.message.clone(),
130 line: warning.line + line_offset,
131 column: warning.column,
132 end_line: warning.end_line + line_offset,
133 end_column: warning.end_column,
134 severity: warning.severity,
135 fix: None,
136 rule_name: warning.rule_name,
137 };
138 all_warnings.push(adjusted_warning);
139 }
140 }
141 }
142 }
143
144 all_warnings
145}
146
147pub fn strip_common_indent(content: &str) -> (String, String) {
150 let lines: Vec<&str> = content.lines().collect();
151 let has_trailing_newline = content.ends_with('\n');
152
153 let min_indent = lines
154 .iter()
155 .filter(|line| !line.trim().is_empty())
156 .map(|line| line.len() - line.trim_start().len())
157 .min()
158 .unwrap_or(0);
159
160 let mut stripped: String = lines
161 .iter()
162 .map(|line| {
163 if line.trim().is_empty() {
164 ""
165 } else if line.len() >= min_indent {
166 &line[min_indent..]
167 } else {
168 line.trim_start()
169 }
170 })
171 .collect::<Vec<_>>()
172 .join("\n");
173
174 if has_trailing_newline && !stripped.ends_with('\n') {
175 stripped.push('\n');
176 }
177
178 let indent_str = " ".repeat(min_indent);
179 (stripped, indent_str)
180}