rumdl_lib/
document_run.rs1use std::borrow::Cow;
4use std::path::{Path, PathBuf};
5
6use crate::config::{Config, MarkdownFlavor};
7use crate::fix_coordinator::{FixCoordinator, FixResult};
8use crate::rule::{LintError, LintWarning, Rule};
9use crate::utils::{LineEnding, detect_line_ending_enum, normalize_line_ending};
10use crate::workspace_index::FileIndex;
11
12pub struct DocumentAnalysis {
14 pub warnings: Vec<LintWarning>,
15 pub file_index: FileIndex,
16}
17
18pub struct DocumentRun<'a> {
25 content: &'a str,
26 rules: &'a [Box<dyn Rule>],
27 config: &'a Config,
28 config_path: Option<&'a Path>,
29 source_file: Option<&'a Path>,
30 link_target_policy: Option<&'a crate::lint_context::LinkTargetPolicy>,
31 verbose: bool,
32}
33
34impl<'a> DocumentRun<'a> {
35 pub fn new(content: &'a str, rules: &'a [Box<dyn Rule>], config: &'a Config) -> Self {
36 Self {
37 content,
38 rules,
39 config,
40 config_path: None,
41 source_file: None,
42 link_target_policy: None,
43 verbose: false,
44 }
45 }
46
47 pub fn file_path(mut self, path: &'a Path) -> Self {
49 self.config_path = Some(path);
50 self.source_file = Some(path);
51 self
52 }
53
54 pub fn config_path(mut self, path: Option<&'a Path>) -> Self {
56 self.config_path = path;
57 self
58 }
59
60 pub fn source_file(mut self, path: Option<&'a Path>) -> Self {
62 self.source_file = path;
63 self
64 }
65
66 pub fn link_target_policy(mut self, policy: &'a crate::lint_context::LinkTargetPolicy) -> Self {
68 self.link_target_policy = Some(policy);
69 self
70 }
71
72 pub fn verbose(mut self, verbose: bool) -> Self {
73 self.verbose = verbose;
74 self
75 }
76
77 pub fn flavor(&self) -> MarkdownFlavor {
78 self.config_path.map_or_else(
79 || self.config.markdown_flavor(),
80 |path| self.config.get_flavor_for_file(path),
81 )
82 }
83
84 pub fn analyze(&self) -> Result<DocumentAnalysis, LintError> {
85 let (warnings, file_index) = self.analyze_raw();
86 warnings.map(|warnings| DocumentAnalysis { warnings, file_index })
87 }
88
89 pub fn analyze_raw(&self) -> (Result<Vec<LintWarning>, LintError>, FileIndex) {
90 crate::lint_and_index_with_paths(
91 self.content,
92 self.rules,
93 self.verbose,
94 self.flavor(),
95 self.paths(),
96 Some(self.config),
97 )
98 }
99
100 pub fn fix(&self, max_iterations: usize) -> Result<(String, FixResult), String> {
110 let line_ending = detect_line_ending_enum(self.content);
111 let normalized = normalize_line_ending(self.content, LineEnding::Lf);
112 let mut content = normalized.to_string();
113 let result = FixCoordinator::new().apply_fixes_iterative_with_paths(
114 self.rules,
115 &[],
116 &mut content,
117 self.config,
118 max_iterations,
119 self.paths(),
120 )?;
121 if content == *normalized {
122 return Ok((self.content.to_string(), result));
123 }
124 let restored = match normalize_line_ending(&content, line_ending) {
125 Cow::Borrowed(_) => content,
126 Cow::Owned(restored) => restored,
127 };
128 Ok((restored, result))
129 }
130
131 pub fn config_path_buf(&self) -> Option<PathBuf> {
132 self.config_path.map(Path::to_path_buf)
133 }
134
135 fn paths(&self) -> crate::DocumentPaths<'a> {
136 crate::DocumentPaths {
137 config_path: self.config_path,
138 source_file: self.source_file,
139 link_target_policy: self.link_target_policy,
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use std::any::Any;
147
148 use indexmap::IndexMap;
149
150 use super::*;
151 use crate::lint_context::LintContext;
152 use crate::rule::{LintResult, Severity};
153
154 #[derive(Clone)]
155 struct ContextProbe;
156
157 impl Rule for ContextProbe {
158 fn name(&self) -> &'static str {
159 "TEST001"
160 }
161
162 fn description(&self) -> &'static str {
163 "Probe document context"
164 }
165
166 fn check(&self, ctx: &LintContext) -> LintResult {
167 let message = format!("flavor={};source={}", ctx.flavor, ctx.source_file().is_some());
168 Ok(vec![LintWarning {
169 message,
170 line: 1,
171 column: 1,
172 end_line: 1,
173 end_column: 1,
174 severity: Severity::Warning,
175 fix: None,
176 rule_name: Some(self.name().to_string()),
177 }])
178 }
179
180 fn fix(&self, ctx: &LintContext) -> Result<String, LintError> {
181 Ok(ctx.content.to_string())
182 }
183
184 fn as_any(&self) -> &dyn Any {
185 self
186 }
187 }
188
189 #[test]
190 fn logical_path_selects_flavor_without_exposing_a_filesystem_path() {
191 let mut config = Config::default();
192 config.per_file_flavor = IndexMap::from([("docs/**".to_string(), MarkdownFlavor::MkDocs)]);
193 let rules: Vec<Box<dyn Rule>> = vec![Box::new(ContextProbe)];
194 let path = Path::new("docs/page.md");
195
196 let analysis = DocumentRun::new("text", &rules, &config)
197 .config_path(Some(path))
198 .analyze()
199 .unwrap();
200
201 assert_eq!(analysis.warnings[0].message, "flavor=mkdocs;source=false");
202 }
203
204 #[test]
205 fn native_file_path_selects_flavor_and_reaches_rules() {
206 let mut config = Config::default();
207 config.per_file_flavor = IndexMap::from([("docs/**".to_string(), MarkdownFlavor::MkDocs)]);
208 let rules: Vec<Box<dyn Rule>> = vec![Box::new(ContextProbe)];
209 let path = Path::new("docs/page.md");
210
211 let analysis = DocumentRun::new("text", &rules, &config)
212 .file_path(path)
213 .analyze()
214 .unwrap();
215
216 assert_eq!(analysis.warnings[0].message, "flavor=mkdocs;source=true");
217 }
218}