1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::utils::range_utils::calculate_match_range;
6
7pub mod md010_config;
8pub use md010_config::MD010Config;
9
10#[derive(Clone, Default)]
12pub struct MD010NoHardTabs {
13 config: MD010Config,
14}
15
16impl MD010NoHardTabs {
17 pub fn new(spaces_per_tab: usize) -> Self {
18 Self {
19 config: MD010Config {
20 spaces_per_tab: crate::types::PositiveUsize::from_const(spaces_per_tab),
21 code_blocks: false,
22 },
23 }
24 }
25
26 pub const fn from_config_struct(config: MD010Config) -> Self {
27 Self { config }
28 }
29
30 fn count_leading_tabs(line: &str) -> usize {
31 let mut count = 0;
32 for c in line.chars() {
33 if c == '\t' {
34 count += 1;
35 } else {
36 break;
37 }
38 }
39 count
40 }
41
42 fn find_and_group_tabs(line: &str) -> Vec<(usize, usize)> {
43 let mut groups = Vec::new();
44 let mut current_group_start: Option<usize> = None;
45 let mut last_tab_pos = 0;
46
47 for (i, c) in line.chars().enumerate() {
48 if c == '\t' {
49 if let Some(start) = current_group_start {
50 if i == last_tab_pos + 1 {
52 last_tab_pos = i;
54 } else {
55 groups.push((start, last_tab_pos + 1));
57 current_group_start = Some(i);
58 last_tab_pos = i;
59 }
60 } else {
61 current_group_start = Some(i);
63 last_tab_pos = i;
64 }
65 }
66 }
67
68 if let Some(start) = current_group_start {
70 groups.push((start, last_tab_pos + 1));
71 }
72
73 groups
74 }
75}
76
77impl Rule for MD010NoHardTabs {
78 fn name(&self) -> &'static str {
79 "MD010"
80 }
81
82 fn description(&self) -> &'static str {
83 "No tabs"
84 }
85
86 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
87 let line_index = &ctx.line_index;
88
89 let mut warnings = Vec::new();
90 let lines = ctx.raw_lines();
91
92 let skip_code_blocks = !self.config.code_blocks;
95
96 for (line_num, &line) in lines.iter().enumerate() {
97 if skip_code_blocks && ctx.line_info(line_num + 1).is_some_and(|info| info.in_code_block) {
98 continue;
99 }
100
101 if ctx.line_info(line_num + 1).is_some_and(|info| {
103 info.in_html_comment
104 || info.in_mdx_comment
105 || info.in_html_block
106 || info.in_pymdown_block
107 || info.in_mkdocstrings
108 || info.in_esm_block
109 }) {
110 continue;
111 }
112
113 let tab_groups = Self::find_and_group_tabs(line);
115 if tab_groups.is_empty() {
116 continue;
117 }
118
119 let leading_tabs = Self::count_leading_tabs(line);
120
121 for (start_pos, end_pos) in tab_groups {
123 let tab_count = end_pos - start_pos;
124 let is_leading = start_pos < leading_tabs;
125
126 let (start_line, start_col, end_line, end_col) =
128 calculate_match_range(line_num + 1, line, start_pos, tab_count);
129
130 let message = if line.trim().is_empty() {
131 if tab_count == 1 {
132 "Empty line contains tab".to_string()
133 } else {
134 format!("Empty line contains {tab_count} tabs")
135 }
136 } else if is_leading {
137 if tab_count == 1 {
138 format!(
139 "Found leading tab, use {} spaces instead",
140 self.config.spaces_per_tab.get()
141 )
142 } else {
143 format!(
144 "Found {} leading tabs, use {} spaces instead",
145 tab_count,
146 tab_count * self.config.spaces_per_tab.get()
147 )
148 }
149 } else if tab_count == 1 {
150 "Found tab for alignment, use spaces instead".to_string()
151 } else {
152 format!("Found {tab_count} tabs for alignment, use spaces instead")
153 };
154
155 warnings.push(LintWarning {
156 rule_name: Some(self.name().to_string()),
157 line: start_line,
158 column: start_col,
159 end_line,
160 end_column: end_col,
161 message,
162 severity: Severity::Warning,
163 fix: Some(Fix::new(
164 line_index.line_col_to_byte_range_with_length(line_num + 1, start_pos + 1, tab_count),
165 " ".repeat(tab_count * self.config.spaces_per_tab.get()),
166 )),
167 });
168 }
169 }
170
171 Ok(warnings)
172 }
173
174 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
175 if self.should_skip(ctx) {
176 return Ok(ctx.content.to_string());
177 }
178 let warnings = self.check(ctx)?;
179 if warnings.is_empty() {
180 return Ok(ctx.content.to_string());
181 }
182 let warnings =
183 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
184 crate::utils::fix_utils::apply_warning_fixes(ctx.content, &warnings)
185 .map_err(crate::rule::LintError::InvalidInput)
186 }
187
188 fn as_any(&self) -> &dyn std::any::Any {
189 self
190 }
191
192 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
193 ctx.content.is_empty() || !ctx.has_char('\t')
195 }
196
197 fn category(&self) -> RuleCategory {
198 RuleCategory::Whitespace
199 }
200
201 crate::impl_rule_config_methods!(MD010Config);
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::lint_context::LintContext;
208 use crate::rule::Rule;
209
210 #[test]
211 fn test_no_tabs() {
212 let rule = MD010NoHardTabs::default();
213 let content = "This is a line\nAnother line\nNo tabs here";
214 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
215 let result = rule.check(&ctx).unwrap();
216 assert!(result.is_empty());
217 }
218
219 #[test]
220 fn test_single_tab() {
221 let rule = MD010NoHardTabs::default();
222 let content = "Line with\ttab";
223 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
224 let result = rule.check(&ctx).unwrap();
225 assert_eq!(result.len(), 1);
226 assert_eq!(result[0].line, 1);
227 assert_eq!(result[0].column, 10);
228 assert_eq!(result[0].message, "Found tab for alignment, use spaces instead");
229 }
230
231 #[test]
232 fn test_leading_tabs_skipped_in_indented_code_by_default() {
233 let content = "\tIndented line\n\t\tDouble indented";
236 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
237
238 let rule_off = MD010NoHardTabs::default();
239 let result_off = rule_off.check(&ctx).unwrap();
240 assert!(
241 result_off.is_empty(),
242 "indented code block skipped by default, got {result_off:?}"
243 );
244 assert_eq!(
245 rule_off.fix(&ctx).unwrap(),
246 "\tIndented line\n\t\tDouble indented",
247 "fix must preserve indented code block content"
248 );
249
250 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
252 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
253 code_blocks: true,
254 });
255 let result_on = rule_on.check(&ctx).unwrap();
256 assert_eq!(result_on.len(), 2, "got {result_on:?}");
257 assert_eq!(result_on[0].line, 1);
258 assert_eq!(result_on[0].message, "Found leading tab, use 4 spaces instead");
259 assert_eq!(result_on[1].line, 2);
260 assert_eq!(result_on[1].message, "Found 2 leading tabs, use 8 spaces instead");
261 assert_eq!(rule_on.fix(&ctx).unwrap(), " Indented line\n Double indented");
262 }
263
264 #[test]
265 fn test_fix_tabs() {
266 let content = "\tIndented\nNormal\tline\nNo tabs";
269 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
270
271 let rule_off = MD010NoHardTabs::default();
272 let warnings_off = rule_off.check(&ctx).unwrap();
273 assert_eq!(warnings_off.len(), 1, "got {warnings_off:?}");
274 assert_eq!(warnings_off[0].line, 2);
275 assert_eq!(warnings_off[0].message, "Found tab for alignment, use spaces instead");
276 assert_eq!(
277 rule_off.fix(&ctx).unwrap(),
278 "\tIndented\nNormal line\nNo tabs",
279 "indented code block line preserved; alignment tab fixed"
280 );
281
282 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
284 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
285 code_blocks: true,
286 });
287 let warnings_on = rule_on.check(&ctx).unwrap();
288 assert_eq!(warnings_on.len(), 2, "got {warnings_on:?}");
289 assert_eq!(warnings_on[0].line, 1);
290 assert_eq!(warnings_on[1].line, 2);
291 assert_eq!(rule_on.fix(&ctx).unwrap(), " Indented\nNormal line\nNo tabs");
292 }
293
294 #[test]
295 fn test_custom_spaces_per_tab() {
296 let content = "\tIndented";
298 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
299
300 let rule_off = MD010NoHardTabs::new(4);
301 assert!(
302 rule_off.check(&ctx).unwrap().is_empty(),
303 "indented code block skipped by default"
304 );
305 assert_eq!(
306 rule_off.fix(&ctx).unwrap(),
307 "\tIndented",
308 "indented code block preserved by default"
309 );
310
311 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
313 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
314 code_blocks: true,
315 });
316 assert_eq!(rule_on.check(&ctx).unwrap().len(), 1);
317 assert_eq!(rule_on.fix(&ctx).unwrap(), " Indented");
318 }
319
320 #[test]
321 fn test_fenced_code_block_tabs_skipped_by_default() {
322 let rule = MD010NoHardTabs::default();
323 let content = "Normal\tline\n```\nCode\twith\ttab\n```\nAnother\tline";
324 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
325 let result = rule.check(&ctx).unwrap();
326 assert_eq!(result.len(), 2);
328 assert_eq!(result[0].line, 1);
329 assert_eq!(result[1].line, 5);
330
331 let fixed = rule.fix(&ctx).unwrap();
332 assert_eq!(fixed, "Normal line\n```\nCode\twith\ttab\n```\nAnother line");
333 }
334
335 #[test]
336 fn test_fenced_only_content_skipped_by_default() {
337 let rule = MD010NoHardTabs::default();
338 let content = "```\nCode\twith\ttab\n```";
339 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
340 let result = rule.check(&ctx).unwrap();
341 assert_eq!(result.len(), 0);
344 }
345
346 #[test]
347 fn test_html_comments_ignored() {
348 let rule = MD010NoHardTabs::default();
349 let content = "Normal\tline\n<!-- HTML\twith\ttab -->\nAnother\tline";
350 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
351 let result = rule.check(&ctx).unwrap();
352 assert_eq!(result.len(), 2);
354 assert_eq!(result[0].line, 1);
355 assert_eq!(result[1].line, 3);
356 }
357
358 #[test]
359 fn test_multiline_html_comments() {
360 let rule = MD010NoHardTabs::default();
361 let content = "Before\n<!--\nMultiline\twith\ttabs\ncomment\t-->\nAfter\ttab";
362 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
363 let result = rule.check(&ctx).unwrap();
364 assert_eq!(result.len(), 1);
366 assert_eq!(result[0].line, 5);
367 }
368
369 #[test]
370 fn test_empty_lines_with_tabs() {
371 let rule = MD010NoHardTabs::default();
372 let content = "Normal line\n\t\t\n\t\nAnother line";
373 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
374 let result = rule.check(&ctx).unwrap();
375 assert_eq!(result.len(), 2);
376 assert_eq!(result[0].message, "Empty line contains 2 tabs");
377 assert_eq!(result[1].message, "Empty line contains tab");
378 }
379
380 #[test]
381 fn test_mixed_tabs_and_spaces() {
382 let content = " \tMixed indentation\n\t Mixed again";
386 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
387
388 let rule_off = MD010NoHardTabs::default();
389 let result_off = rule_off.check(&ctx).unwrap();
390 assert!(
391 result_off.is_empty(),
392 "indented code block lines skipped, got {result_off:?}"
393 );
394 assert_eq!(
395 rule_off.fix(&ctx).unwrap(),
396 " \tMixed indentation\n\t Mixed again",
397 "content preserved unchanged"
398 );
399
400 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
402 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
403 code_blocks: true,
404 });
405 let result_on = rule_on.check(&ctx).unwrap();
406 assert_eq!(result_on.len(), 2, "got {result_on:?}");
407 assert_eq!(rule_on.fix(&ctx).unwrap(), " Mixed indentation\n Mixed again");
408 }
409
410 #[test]
411 fn test_consecutive_tabs() {
412 let rule = MD010NoHardTabs::default();
413 let content = "Text\t\t\tthree tabs\tand\tanother";
414 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
415 let result = rule.check(&ctx).unwrap();
416 assert_eq!(result.len(), 3);
418 assert_eq!(result[0].message, "Found 3 tabs for alignment, use spaces instead");
419 }
420
421 #[test]
422 fn test_find_and_group_tabs() {
423 let groups = MD010NoHardTabs::find_and_group_tabs("a\tb\tc");
425 assert_eq!(groups, vec![(1, 2), (3, 4)]);
426
427 let groups = MD010NoHardTabs::find_and_group_tabs("\t\tabc");
428 assert_eq!(groups, vec![(0, 2)]);
429
430 let groups = MD010NoHardTabs::find_and_group_tabs("no tabs");
431 assert!(groups.is_empty());
432
433 let groups = MD010NoHardTabs::find_and_group_tabs("\t\t\ta\t\tb");
435 assert_eq!(groups, vec![(0, 3), (4, 6)]);
436
437 let groups = MD010NoHardTabs::find_and_group_tabs("\ta\tb\tc");
438 assert_eq!(groups, vec![(0, 1), (2, 3), (4, 5)]);
439 }
440
441 #[test]
442 fn test_count_leading_tabs() {
443 assert_eq!(MD010NoHardTabs::count_leading_tabs("\t\tcode"), 2);
444 assert_eq!(MD010NoHardTabs::count_leading_tabs(" \tcode"), 0);
445 assert_eq!(MD010NoHardTabs::count_leading_tabs("no tabs"), 0);
446 assert_eq!(MD010NoHardTabs::count_leading_tabs("\t"), 1);
447 }
448
449 #[test]
450 fn test_default_config() {
451 let rule = MD010NoHardTabs::default();
452 let config = rule.default_config_section();
453 assert!(config.is_some());
454 let (name, _value) = config.unwrap();
455 assert_eq!(name, "MD010");
456 }
457
458 #[test]
459 fn test_from_config() {
460 let content_plain = "\tTab";
462 let ctx_plain = LintContext::new(content_plain, crate::config::MarkdownFlavor::Standard, None);
463 let rule_8_off = MD010NoHardTabs::new(8); assert!(
465 rule_8_off.check(&ctx_plain).unwrap().is_empty(),
466 "indented code block skipped"
467 );
468 assert_eq!(
469 rule_8_off.fix(&ctx_plain).unwrap(),
470 "\tTab",
471 "content preserved unchanged"
472 );
473
474 let rule_8_on = MD010NoHardTabs::from_config_struct(MD010Config {
476 spaces_per_tab: crate::types::PositiveUsize::from_const(8),
477 code_blocks: true,
478 });
479 assert_eq!(rule_8_on.check(&ctx_plain).unwrap().len(), 1);
480 assert_eq!(rule_8_on.fix(&ctx_plain).unwrap(), " Tab");
481
482 let content_fenced = "```\n\tTab in code\n```";
484 let ctx_fenced = LintContext::new(content_fenced, crate::config::MarkdownFlavor::Standard, None);
485 assert!(
486 rule_8_off.check(&ctx_fenced).unwrap().is_empty(),
487 "fenced code block skipped"
488 );
489 assert_eq!(rule_8_off.fix(&ctx_fenced).unwrap(), "```\n\tTab in code\n```");
490
491 let result_on = rule_8_on.check(&ctx_fenced).unwrap();
493 assert_eq!(result_on.len(), 1, "got {result_on:?}");
494 assert_eq!(result_on[0].line, 2);
495 assert_eq!(rule_8_on.fix(&ctx_fenced).unwrap(), "```\n Tab in code\n```");
496 }
497
498 #[test]
499 fn test_performance_large_document() {
500 let rule = MD010NoHardTabs::default();
501 let mut content = String::new();
502 for i in 0..1000 {
503 content.push_str(&format!("Line {i}\twith\ttabs\n"));
504 }
505 let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
506 let result = rule.check(&ctx).unwrap();
507 assert_eq!(result.len(), 2000);
508 }
509
510 #[test]
511 fn test_preserve_content() {
512 let rule = MD010NoHardTabs::default();
513 let content = "**Bold**\ttext\n*Italic*\ttext\n[Link](url)\ttab";
514 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
515 let fixed = rule.fix(&ctx).unwrap();
516 assert_eq!(fixed, "**Bold** text\n*Italic* text\n[Link](url) tab");
517 }
518
519 #[test]
520 fn test_edge_cases() {
521 let rule = MD010NoHardTabs::default();
522
523 let content = "Text\t";
525 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
526 let result = rule.check(&ctx).unwrap();
527 assert_eq!(result.len(), 1);
528
529 let content = "\t\t\t";
531 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
532 let result = rule.check(&ctx).unwrap();
533 assert_eq!(result.len(), 1);
534 assert_eq!(result[0].message, "Empty line contains 3 tabs");
535 }
536
537 #[test]
538 fn test_fenced_code_block_tabs_preserved_in_fix_by_default() {
539 let rule = MD010NoHardTabs::default();
540
541 let content = "Text\twith\ttab\n```makefile\ntarget:\n\tcommand\n\tanother\n```\nMore\ttabs";
542 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
543 let fixed = rule.fix(&ctx).unwrap();
544
545 let expected = "Text with tab\n```makefile\ntarget:\n\tcommand\n\tanother\n```\nMore tabs";
548 assert_eq!(fixed, expected);
549 }
550
551 #[test]
552 fn test_tilde_fence_longer_than_3() {
553 let rule = MD010NoHardTabs::default();
554 let content = "~~~~~\ncode\twith\ttab\n~~~~~\ntext\twith\ttab";
556 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
557 let result = rule.check(&ctx).unwrap();
558 assert_eq!(
560 result.len(),
561 2,
562 "Expected 2 warnings but got {}: {:?}",
563 result.len(),
564 result
565 );
566 assert_eq!(result[0].line, 4);
567 assert_eq!(result[1].line, 4);
568 }
569
570 #[test]
571 fn test_backtick_fence_longer_than_3() {
572 let rule = MD010NoHardTabs::default();
573 let content = "`````\ncode\twith\ttab\n`````\ntext\twith\ttab";
575 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
576 let result = rule.check(&ctx).unwrap();
577 assert_eq!(
578 result.len(),
579 2,
580 "Expected 2 warnings but got {}: {:?}",
581 result.len(),
582 result
583 );
584 assert_eq!(result[0].line, 4);
585 assert_eq!(result[1].line, 4);
586 }
587
588 #[test]
589 fn test_indented_code_block_tabs_skipped_by_default() {
590 let content = " code\twith\ttab\n\nNormal\ttext";
593 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
594
595 let rule_off = MD010NoHardTabs::default();
596 let result_off = rule_off.check(&ctx).unwrap();
597 assert_eq!(
598 result_off.len(),
599 1,
600 "expected 1 warning (only normal-text tab), got {}: {:?}",
601 result_off.len(),
602 result_off
603 );
604 assert_eq!(result_off[0].line, 3);
605 assert_eq!(result_off[0].message, "Found tab for alignment, use spaces instead");
606
607 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
609 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
610 code_blocks: true,
611 });
612 let result_on = rule_on.check(&ctx).unwrap();
613 assert_eq!(
614 result_on.len(),
615 3,
616 "expected 3 warnings with code_blocks=true, got {}: {:?}",
617 result_on.len(),
618 result_on
619 );
620 assert_eq!(result_on[0].line, 1);
621 assert_eq!(result_on[1].line, 1);
622 assert_eq!(result_on[2].line, 3);
623 }
624
625 #[test]
626 fn test_html_comment_end_then_start_same_line() {
627 let rule = MD010NoHardTabs::default();
628 let content =
630 "<!-- first comment\nend --> text <!-- second comment\n\ttabbed content inside second comment\n-->";
631 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
632 let result = rule.check(&ctx).unwrap();
633 assert!(
634 result.is_empty(),
635 "Expected 0 warnings but got {}: {:?}",
636 result.len(),
637 result
638 );
639 }
640
641 #[test]
642 fn test_fix_tilde_fence_longer_than_3() {
643 let rule = MD010NoHardTabs::default();
644 let content = "~~~~~\ncode\twith\ttab\n~~~~~\ntext\twith\ttab";
645 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
646 let fixed = rule.fix(&ctx).unwrap();
647 assert_eq!(fixed, "~~~~~\ncode\twith\ttab\n~~~~~\ntext with tab");
649 }
650
651 #[test]
652 fn test_fix_indented_code_block_tabs_replaced() {
653 let content = " code\twith\ttab\n\nNormal\ttext";
655 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
656
657 let rule_off = MD010NoHardTabs::default();
658 assert_eq!(
659 rule_off.fix(&ctx).unwrap(),
660 " code\twith\ttab\n\nNormal text",
661 "indented code block preserved; only normal-text tab fixed"
662 );
663
664 let rule_on = MD010NoHardTabs::from_config_struct(MD010Config {
666 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
667 code_blocks: true,
668 });
669 assert_eq!(
670 rule_on.fix(&ctx).unwrap(),
671 " code with tab\n\nNormal text",
672 "all tabs replaced with code_blocks=true"
673 );
674 }
675
676 #[test]
677 fn test_issue_630_default_skips_both_code_blocks() {
678 let rule = MD010NoHardTabs::default();
680 let content = "Foo bar\n\n for range 100 {\n \tfoo()\n }\n\nThis is a fenced\n\n```\nfor range 100 {\n\tfoo()\n}\n```\n";
681 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
682 let result = rule.check(&ctx).unwrap();
683 assert!(result.is_empty(), "both code blocks skipped, got {result:?}");
684 }
685
686 #[test]
687 fn test_issue_630_code_blocks_true_flags_both() {
688 let rule = MD010NoHardTabs::from_config_struct(MD010Config {
690 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
691 code_blocks: true,
692 });
693 let content = "Foo bar\n\n for range 100 {\n \tfoo()\n }\n\nThis is a fenced\n\n```\nfor range 100 {\n\tfoo()\n}\n```\n";
694 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
695 let result = rule.check(&ctx).unwrap();
696 assert_eq!(result.len(), 2, "got {result:?}");
699 assert_eq!(result[0].line, 4);
700 assert_eq!(result[1].line, 11);
701 }
702
703 #[test]
704 fn test_code_blocks_toggle_fenced() {
705 let content = "Normal\tline\n```\nCode\twith\ttab\n```\nAnother\tline";
706
707 let off = MD010NoHardTabs::default();
709 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
710 let r_off = off.check(&ctx).unwrap();
711 assert_eq!(r_off.len(), 2, "got {r_off:?}");
712 assert_eq!(r_off[0].line, 1);
713 assert_eq!(r_off[1].line, 5);
714 assert_eq!(
715 off.fix(&ctx).unwrap(),
716 "Normal line\n```\nCode\twith\ttab\n```\nAnother line"
717 );
718
719 let on = MD010NoHardTabs::from_config_struct(MD010Config {
721 spaces_per_tab: crate::types::PositiveUsize::from_const(4),
722 code_blocks: true,
723 });
724 let r_on = on.check(&ctx).unwrap();
725 assert_eq!(r_on.len(), 4, "got {r_on:?}");
726 assert_eq!(r_on[0].line, 1);
727 assert_eq!(r_on[1].line, 3);
728 assert_eq!(r_on[2].line, 3);
729 assert_eq!(r_on[3].line, 5);
730 assert_eq!(
731 on.fix(&ctx).unwrap(),
732 "Normal line\n```\nCode with tab\n```\nAnother line"
733 );
734 }
735
736 #[test]
737 fn test_code_blocks_toggle_makefile_fence_preserved_by_default() {
738 let content = "Text\twith\ttab\n```makefile\ntarget:\n\tcommand\n```\nMore\ttabs";
739 let off = MD010NoHardTabs::default();
740 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
741 assert_eq!(
743 off.fix(&ctx).unwrap(),
744 "Text with tab\n```makefile\ntarget:\n\tcommand\n```\nMore tabs"
745 );
746 }
747}