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)]
62pub struct LineFilterConfig {
63 pub skip_front_matter: bool,
65 pub skip_code_blocks: bool,
67 pub skip_html_blocks: bool,
69 pub skip_html_comments: bool,
71}
72
73impl LineFilterConfig {
74 #[must_use]
76 pub fn new() -> Self {
77 Self::default()
78 }
79
80 #[must_use]
85 pub fn skip_front_matter(mut self) -> Self {
86 self.skip_front_matter = true;
87 self
88 }
89
90 #[must_use]
95 pub fn skip_code_blocks(mut self) -> Self {
96 self.skip_code_blocks = true;
97 self
98 }
99
100 #[must_use]
105 pub fn skip_html_blocks(mut self) -> Self {
106 self.skip_html_blocks = true;
107 self
108 }
109
110 #[must_use]
115 pub fn skip_html_comments(mut self) -> Self {
116 self.skip_html_comments = true;
117 self
118 }
119
120 fn should_filter(&self, line_info: &LineInfo) -> bool {
122 (self.skip_front_matter && line_info.in_front_matter)
123 || (self.skip_code_blocks && line_info.in_code_block)
124 || (self.skip_html_blocks && line_info.in_html_block)
125 || (self.skip_html_comments && line_info.in_html_comment)
126 }
127}
128
129pub struct FilteredLinesIter<'a> {
131 ctx: &'a LintContext<'a>,
132 config: LineFilterConfig,
133 current_index: usize,
134}
135
136impl<'a> FilteredLinesIter<'a> {
137 fn new(ctx: &'a LintContext<'a>, config: LineFilterConfig) -> Self {
139 Self {
140 ctx,
141 config,
142 current_index: 0,
143 }
144 }
145}
146
147impl<'a> Iterator for FilteredLinesIter<'a> {
148 type Item = FilteredLine<'a>;
149
150 fn next(&mut self) -> Option<Self::Item> {
151 let lines = &self.ctx.lines;
152 let content_lines: Vec<&str> = self.ctx.content.lines().collect();
153
154 while self.current_index < lines.len() {
155 let idx = self.current_index;
156 self.current_index += 1;
157
158 if self.config.should_filter(&lines[idx]) {
160 continue;
161 }
162
163 let line_content = content_lines.get(idx).copied().unwrap_or("");
165
166 return Some(FilteredLine {
168 line_num: idx + 1, line_info: &lines[idx],
170 content: line_content,
171 });
172 }
173
174 None
175 }
176}
177
178pub trait FilteredLinesExt {
183 fn filtered_lines(&self) -> FilteredLinesBuilder<'_>;
202
203 fn content_lines(&self) -> FilteredLinesIter<'_>;
226}
227
228pub struct FilteredLinesBuilder<'a> {
230 ctx: &'a LintContext<'a>,
231 config: LineFilterConfig,
232}
233
234impl<'a> FilteredLinesBuilder<'a> {
235 fn new(ctx: &'a LintContext<'a>) -> Self {
236 Self {
237 ctx,
238 config: LineFilterConfig::new(),
239 }
240 }
241
242 #[must_use]
244 pub fn skip_front_matter(mut self) -> Self {
245 self.config = self.config.skip_front_matter();
246 self
247 }
248
249 #[must_use]
251 pub fn skip_code_blocks(mut self) -> Self {
252 self.config = self.config.skip_code_blocks();
253 self
254 }
255
256 #[must_use]
258 pub fn skip_html_blocks(mut self) -> Self {
259 self.config = self.config.skip_html_blocks();
260 self
261 }
262
263 #[must_use]
265 pub fn skip_html_comments(mut self) -> Self {
266 self.config = self.config.skip_html_comments();
267 self
268 }
269}
270
271impl<'a> IntoIterator for FilteredLinesBuilder<'a> {
272 type Item = FilteredLine<'a>;
273 type IntoIter = FilteredLinesIter<'a>;
274
275 fn into_iter(self) -> Self::IntoIter {
276 FilteredLinesIter::new(self.ctx, self.config)
277 }
278}
279
280impl<'a> FilteredLinesExt for LintContext<'a> {
281 fn filtered_lines(&self) -> FilteredLinesBuilder<'_> {
282 FilteredLinesBuilder::new(self)
283 }
284
285 fn content_lines(&self) -> FilteredLinesIter<'_> {
286 FilteredLinesIter::new(self, LineFilterConfig::new().skip_front_matter())
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use crate::config::MarkdownFlavor;
294
295 #[test]
296 fn test_filtered_line_structure() {
297 let content = "# Title\n\nContent";
298 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
299
300 let line = ctx.content_lines().next().unwrap();
301 assert_eq!(line.line_num, 1);
302 assert_eq!(line.content, "# Title");
303 assert!(!line.line_info.in_front_matter);
304 }
305
306 #[test]
307 fn test_skip_front_matter_yaml() {
308 let content = "---\ntitle: Test\nurl: http://example.com\n---\n\n# Content\n\nMore content";
309 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
310
311 let lines: Vec<_> = ctx.content_lines().collect();
312 assert_eq!(lines.len(), 4);
314 assert_eq!(lines[0].line_num, 5); assert_eq!(lines[0].content, "");
316 assert_eq!(lines[1].line_num, 6);
317 assert_eq!(lines[1].content, "# Content");
318 assert_eq!(lines[2].line_num, 7);
319 assert_eq!(lines[2].content, "");
320 assert_eq!(lines[3].line_num, 8);
321 assert_eq!(lines[3].content, "More content");
322 }
323
324 #[test]
325 fn test_skip_front_matter_toml() {
326 let content = "+++\ntitle = \"Test\"\nurl = \"http://example.com\"\n+++\n\n# Content";
327 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
328
329 let lines: Vec<_> = ctx.content_lines().collect();
330 assert_eq!(lines.len(), 2); assert_eq!(lines[0].line_num, 5);
332 assert_eq!(lines[1].line_num, 6);
333 assert_eq!(lines[1].content, "# Content");
334 }
335
336 #[test]
337 fn test_skip_front_matter_json() {
338 let content = "{\n\"title\": \"Test\",\n\"url\": \"http://example.com\"\n}\n\n# Content";
339 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
340
341 let lines: Vec<_> = ctx.content_lines().collect();
342 assert_eq!(lines.len(), 2); assert_eq!(lines[0].line_num, 5);
344 assert_eq!(lines[1].line_num, 6);
345 assert_eq!(lines[1].content, "# Content");
346 }
347
348 #[test]
349 fn test_skip_code_blocks() {
350 let content = "# Title\n\n```rust\nlet x = 1;\nlet y = 2;\n```\n\nContent";
351 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
352
353 let lines: Vec<_> = ctx.filtered_lines().skip_code_blocks().into_iter().collect();
354
355 assert!(lines.iter().any(|l| l.content == "# Title"));
360 assert!(lines.iter().any(|l| l.content == "Content"));
361 assert!(!lines.iter().any(|l| l.content == "let x = 1;"));
363 assert!(!lines.iter().any(|l| l.content == "let y = 2;"));
364 }
365
366 #[test]
367 fn test_no_filters() {
368 let content = "---\ntitle: Test\n---\n\n# Content";
369 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
370
371 let lines: Vec<_> = ctx.filtered_lines().into_iter().collect();
373 assert_eq!(lines.len(), ctx.lines.len());
374 }
375
376 #[test]
377 fn test_multiple_filters() {
378 let content = "---\ntitle: Test\n---\n\n# Title\n\n```rust\ncode\n```\n\nContent";
379 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
380
381 let lines: Vec<_> = ctx
382 .filtered_lines()
383 .skip_front_matter()
384 .skip_code_blocks()
385 .into_iter()
386 .collect();
387
388 assert!(lines.iter().any(|l| l.content == "# Title"));
390 assert!(lines.iter().any(|l| l.content == "Content"));
391 assert!(!lines.iter().any(|l| l.content == "title: Test"));
392 assert!(!lines.iter().any(|l| l.content == "code"));
393 }
394
395 #[test]
396 fn test_line_numbering_is_1_indexed() {
397 let content = "First\nSecond\nThird";
398 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
399
400 let lines: Vec<_> = ctx.content_lines().collect();
401 assert_eq!(lines[0].line_num, 1);
402 assert_eq!(lines[0].content, "First");
403 assert_eq!(lines[1].line_num, 2);
404 assert_eq!(lines[1].content, "Second");
405 assert_eq!(lines[2].line_num, 3);
406 assert_eq!(lines[2].content, "Third");
407 }
408
409 #[test]
410 fn test_content_lines_convenience_method() {
411 let content = "---\nfoo: bar\n---\n\nContent";
412 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
413
414 let lines: Vec<_> = ctx.content_lines().collect();
416 assert!(!lines.iter().any(|l| l.content.contains("foo")));
417 assert!(lines.iter().any(|l| l.content == "Content"));
418 }
419
420 #[test]
421 fn test_empty_document() {
422 let content = "";
423 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
424
425 let lines: Vec<_> = ctx.content_lines().collect();
426 assert_eq!(lines.len(), 0);
427 }
428
429 #[test]
430 fn test_only_front_matter() {
431 let content = "---\ntitle: Test\n---";
432 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
433
434 let lines: Vec<_> = ctx.content_lines().collect();
435 assert_eq!(
436 lines.len(),
437 0,
438 "Document with only front matter should have no content lines"
439 );
440 }
441
442 #[test]
443 fn test_builder_pattern_ergonomics() {
444 let content = "# Title\n\n```\ncode\n```\n\nContent";
445 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
446
447 let _lines: Vec<_> = ctx
449 .filtered_lines()
450 .skip_front_matter()
451 .skip_code_blocks()
452 .skip_html_blocks()
453 .into_iter()
454 .collect();
455
456 }
458
459 #[test]
460 fn test_filtered_line_access_to_line_info() {
461 let content = "# Title\n\nContent";
462 let ctx = LintContext::new(content, MarkdownFlavor::Standard);
463
464 for line in ctx.content_lines() {
465 assert!(!line.line_info.in_front_matter);
467 assert!(!line.line_info.in_code_block);
468 }
469 }
470}