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