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