rumdl_lib/
filtered_lines.rs1use crate::lint_context::{LineInfo, LintContext};
36
37#[derive(Debug, Clone)]
39pub struct FilteredLine<'a> {
40 pub line_num: usize,
42 pub line_info: &'a LineInfo,
44 pub content: &'a str,
46}
47
48#[derive(Debug, Clone, Default)]
61pub struct LineFilterConfig {
62 pub skip_front_matter: bool,
64 pub skip_code_blocks: bool,
66 pub skip_html_blocks: bool,
68}
69
70impl LineFilterConfig {
71 #[must_use]
73 pub fn new() -> Self {
74 Self::default()
75 }
76
77 #[must_use]
82 pub fn skip_front_matter(mut self) -> Self {
83 self.skip_front_matter = true;
84 self
85 }
86
87 #[must_use]
92 pub fn skip_code_blocks(mut self) -> Self {
93 self.skip_code_blocks = true;
94 self
95 }
96
97 #[must_use]
102 pub fn skip_html_blocks(mut self) -> Self {
103 self.skip_html_blocks = true;
104 self
105 }
106
107 fn should_filter(&self, line_info: &LineInfo) -> bool {
109 (self.skip_front_matter && line_info.in_front_matter)
110 || (self.skip_code_blocks && line_info.in_code_block)
111 || (self.skip_html_blocks && line_info.in_html_block)
112 }
113}
114
115pub struct FilteredLinesIter<'a> {
117 ctx: &'a LintContext<'a>,
118 config: LineFilterConfig,
119 current_index: usize,
120}
121
122impl<'a> FilteredLinesIter<'a> {
123 fn new(ctx: &'a LintContext<'a>, config: LineFilterConfig) -> Self {
125 Self {
126 ctx,
127 config,
128 current_index: 0,
129 }
130 }
131}
132
133impl<'a> Iterator for FilteredLinesIter<'a> {
134 type Item = FilteredLine<'a>;
135
136 fn next(&mut self) -> Option<Self::Item> {
137 let lines = &self.ctx.lines;
138 let content_lines: Vec<&str> = self.ctx.content.lines().collect();
139
140 while self.current_index < lines.len() {
141 let idx = self.current_index;
142 self.current_index += 1;
143
144 if self.config.should_filter(&lines[idx]) {
146 continue;
147 }
148
149 let line_content = content_lines.get(idx).copied().unwrap_or("");
151
152 return Some(FilteredLine {
154 line_num: idx + 1, line_info: &lines[idx],
156 content: line_content,
157 });
158 }
159
160 None
161 }
162}
163
164pub trait FilteredLinesExt {
169 fn filtered_lines(&self) -> FilteredLinesBuilder<'_>;
188
189 fn content_lines(&self) -> FilteredLinesIter<'_>;
212}
213
214pub struct FilteredLinesBuilder<'a> {
216 ctx: &'a LintContext<'a>,
217 config: LineFilterConfig,
218}
219
220impl<'a> FilteredLinesBuilder<'a> {
221 fn new(ctx: &'a LintContext<'a>) -> Self {
222 Self {
223 ctx,
224 config: LineFilterConfig::new(),
225 }
226 }
227
228 #[must_use]
230 pub fn skip_front_matter(mut self) -> Self {
231 self.config = self.config.skip_front_matter();
232 self
233 }
234
235 #[must_use]
237 pub fn skip_code_blocks(mut self) -> Self {
238 self.config = self.config.skip_code_blocks();
239 self
240 }
241
242 #[must_use]
244 pub fn skip_html_blocks(mut self) -> Self {
245 self.config = self.config.skip_html_blocks();
246 self
247 }
248}
249
250impl<'a> IntoIterator for FilteredLinesBuilder<'a> {
251 type Item = FilteredLine<'a>;
252 type IntoIter = FilteredLinesIter<'a>;
253
254 fn into_iter(self) -> Self::IntoIter {
255 FilteredLinesIter::new(self.ctx, self.config)
256 }
257}
258
259impl<'a> FilteredLinesExt for LintContext<'a> {
260 fn filtered_lines(&self) -> FilteredLinesBuilder<'_> {
261 FilteredLinesBuilder::new(self)
262 }
263
264 fn content_lines(&self) -> FilteredLinesIter<'_> {
265 FilteredLinesIter::new(self, LineFilterConfig::new().skip_front_matter())
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 use crate::config::MarkdownFlavor;
273
274 #[test]
275 fn test_filtered_line_structure() {
276 let content = "# Title\n\nContent";
277 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
278
279 let line = ctx.content_lines().next().unwrap();
280 assert_eq!(line.line_num, 1);
281 assert_eq!(line.content, "# Title");
282 assert!(!line.line_info.in_front_matter);
283 }
284
285 #[test]
286 fn test_skip_front_matter_yaml() {
287 let content = "---\ntitle: Test\nurl: http://example.com\n---\n\n# Content\n\nMore content";
288 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
289
290 let lines: Vec<_> = ctx.content_lines().collect();
291 assert_eq!(lines.len(), 4);
293 assert_eq!(lines[0].line_num, 5); assert_eq!(lines[0].content, "");
295 assert_eq!(lines[1].line_num, 6);
296 assert_eq!(lines[1].content, "# Content");
297 assert_eq!(lines[2].line_num, 7);
298 assert_eq!(lines[2].content, "");
299 assert_eq!(lines[3].line_num, 8);
300 assert_eq!(lines[3].content, "More content");
301 }
302
303 #[test]
304 fn test_skip_front_matter_toml() {
305 let content = "+++\ntitle = \"Test\"\nurl = \"http://example.com\"\n+++\n\n# Content";
306 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
307
308 let lines: Vec<_> = ctx.content_lines().collect();
309 assert_eq!(lines.len(), 2); assert_eq!(lines[0].line_num, 5);
311 assert_eq!(lines[1].line_num, 6);
312 assert_eq!(lines[1].content, "# Content");
313 }
314
315 #[test]
316 fn test_skip_front_matter_json() {
317 let content = "{\n\"title\": \"Test\",\n\"url\": \"http://example.com\"\n}\n\n# Content";
318 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
319
320 let lines: Vec<_> = ctx.content_lines().collect();
321 assert_eq!(lines.len(), 2); assert_eq!(lines[0].line_num, 5);
323 assert_eq!(lines[1].line_num, 6);
324 assert_eq!(lines[1].content, "# Content");
325 }
326
327 #[test]
328 fn test_skip_code_blocks() {
329 let content = "# Title\n\n```rust\nlet x = 1;\nlet y = 2;\n```\n\nContent";
330 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
331
332 let lines: Vec<_> = ctx.filtered_lines().skip_code_blocks().into_iter().collect();
333
334 assert!(lines.iter().any(|l| l.content == "# Title"));
339 assert!(lines.iter().any(|l| l.content == "Content"));
340 assert!(!lines.iter().any(|l| l.content == "let x = 1;"));
342 assert!(!lines.iter().any(|l| l.content == "let y = 2;"));
343 }
344
345 #[test]
346 fn test_no_filters() {
347 let content = "---\ntitle: Test\n---\n\n# Content";
348 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
349
350 let lines: Vec<_> = ctx.filtered_lines().into_iter().collect();
352 assert_eq!(lines.len(), ctx.lines.len());
353 }
354
355 #[test]
356 fn test_multiple_filters() {
357 let content = "---\ntitle: Test\n---\n\n# Title\n\n```rust\ncode\n```\n\nContent";
358 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
359
360 let lines: Vec<_> = ctx
361 .filtered_lines()
362 .skip_front_matter()
363 .skip_code_blocks()
364 .into_iter()
365 .collect();
366
367 assert!(lines.iter().any(|l| l.content == "# Title"));
369 assert!(lines.iter().any(|l| l.content == "Content"));
370 assert!(!lines.iter().any(|l| l.content == "title: Test"));
371 assert!(!lines.iter().any(|l| l.content == "code"));
372 }
373
374 #[test]
375 fn test_line_numbering_is_1_indexed() {
376 let content = "First\nSecond\nThird";
377 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
378
379 let lines: Vec<_> = ctx.content_lines().collect();
380 assert_eq!(lines[0].line_num, 1);
381 assert_eq!(lines[0].content, "First");
382 assert_eq!(lines[1].line_num, 2);
383 assert_eq!(lines[1].content, "Second");
384 assert_eq!(lines[2].line_num, 3);
385 assert_eq!(lines[2].content, "Third");
386 }
387
388 #[test]
389 fn test_content_lines_convenience_method() {
390 let content = "---\nfoo: bar\n---\n\nContent";
391 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
392
393 let lines: Vec<_> = ctx.content_lines().collect();
395 assert!(!lines.iter().any(|l| l.content.contains("foo")));
396 assert!(lines.iter().any(|l| l.content == "Content"));
397 }
398
399 #[test]
400 fn test_empty_document() {
401 let content = "";
402 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
403
404 let lines: Vec<_> = ctx.content_lines().collect();
405 assert_eq!(lines.len(), 0);
406 }
407
408 #[test]
409 fn test_only_front_matter() {
410 let content = "---\ntitle: Test\n---";
411 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
412
413 let lines: Vec<_> = ctx.content_lines().collect();
414 assert_eq!(
415 lines.len(),
416 0,
417 "Document with only front matter should have no content lines"
418 );
419 }
420
421 #[test]
422 fn test_builder_pattern_ergonomics() {
423 let content = "# Title\n\n```\ncode\n```\n\nContent";
424 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
425
426 let _lines: Vec<_> = ctx
428 .filtered_lines()
429 .skip_front_matter()
430 .skip_code_blocks()
431 .skip_html_blocks()
432 .into_iter()
433 .collect();
434
435 }
437
438 #[test]
439 fn test_filtered_line_access_to_line_info() {
440 let content = "# Title\n\nContent";
441 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
442
443 for line in ctx.content_lines() {
444 assert!(!line.line_info.in_front_matter);
446 assert!(!line.line_info.in_code_block);
447 }
448 }
449}