Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package rumdl for openSUSE:Factory checked in at 2026-05-20 15:25:36 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/rumdl (Old) and /work/SRC/openSUSE:Factory/.rumdl.new.1966 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "rumdl" Wed May 20 15:25:36 2026 rev:68 rq:1354149 version:0.1.95 Changes: -------- --- /work/SRC/openSUSE:Factory/rumdl/rumdl.changes 2026-05-18 17:50:34.682654285 +0200 +++ /work/SRC/openSUSE:Factory/.rumdl.new.1966/rumdl.changes 2026-05-20 15:26:52.043939781 +0200 @@ -1,0 +2,13 @@ +Wed May 20 05:08:48 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.1.95: + * Added + - md010: add code_blocks config option, consistent code-block + handling (#630) (b98ca52) + - md010: add code_blocks config option (default false) + (2c95e17) + * Fixed + - md010: treat fenced and indented code blocks consistently + (#630) (435df34) + +------------------------------------------------------------------- Old: ---- rumdl-0.1.94.obscpio New: ---- rumdl-0.1.95.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ rumdl.spec ++++++ --- /var/tmp/diff_new_pack.RCk5Qs/_old 2026-05-20 15:26:53.604004059 +0200 +++ /var/tmp/diff_new_pack.RCk5Qs/_new 2026-05-20 15:26:53.608004224 +0200 @@ -17,7 +17,7 @@ Name: rumdl -Version: 0.1.94 +Version: 0.1.95 Release: 0 Summary: Markdown Linter written in Rust License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.RCk5Qs/_old 2026-05-20 15:26:53.644005707 +0200 +++ /var/tmp/diff_new_pack.RCk5Qs/_new 2026-05-20 15:26:53.648005872 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/rvben/rumdl.git</param> <param name="scm">git</param> <param name="submodules">enable</param> - <param name="revision">v0.1.94</param> + <param name="revision">v0.1.95</param> <param name="match-tag">v*.*.*</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.RCk5Qs/_old 2026-05-20 15:26:53.676007025 +0200 +++ /var/tmp/diff_new_pack.RCk5Qs/_new 2026-05-20 15:26:53.680007190 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/rvben/rumdl.git</param> - <param name="changesrevision">fbb90ac101470d2c249e3bc5d9a8915de477f3e4</param></service></servicedata> + <param name="changesrevision">b2164bb1b33cbc3b416f686180033b8be7374f37</param></service></servicedata> (No newline at EOF) ++++++ rumdl-0.1.94.obscpio -> rumdl-0.1.95.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/CHANGELOG.md new/rumdl-0.1.95/CHANGELOG.md --- old/rumdl-0.1.94/CHANGELOG.md 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/CHANGELOG.md 2026-05-19 22:15:04.000000000 +0200 @@ -37,6 +37,18 @@ + +## [0.1.95](https://github.com/rvben/rumdl/compare/v0.1.94...v0.1.95) - 2026-05-19 + +### Added + +- **md010**: add code_blocks config option, consistent code-block handling (#630) ([b98ca52](https://github.com/rvben/rumdl/commit/b98ca52b73cecd923c03ce98f505a5afefe20435)) +- **md010**: add code_blocks config option (default false) ([2c95e17](https://github.com/rvben/rumdl/commit/2c95e1786a1aaf63af8c767f57b469c146982ae7)) + +### Fixed + +- **md010**: treat fenced and indented code blocks consistently (#630) ([435df34](https://github.com/rvben/rumdl/commit/435df34db542402a2e2f07db12308391bf41a032)) + ## [0.1.94](https://github.com/rvben/rumdl/compare/v0.1.93...v0.1.94) - 2026-05-18 ### Added diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/Cargo.lock new/rumdl-0.1.95/Cargo.lock --- old/rumdl-0.1.94/Cargo.lock 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/Cargo.lock 2026-05-19 22:15:04.000000000 +0200 @@ -2274,7 +2274,7 @@ [[package]] name = "rumdl" -version = "0.1.94" +version = "0.1.95" dependencies = [ "anyhow", "assert_cmd", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/Cargo.toml new/rumdl-0.1.95/Cargo.toml --- old/rumdl-0.1.94/Cargo.toml 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/Cargo.toml 2026-05-19 22:15:04.000000000 +0200 @@ -1,6 +1,6 @@ [package] name = "rumdl" -version = "0.1.94" +version = "0.1.95" edition = "2024" rust-version = "1.94.0" description = "A fast Markdown linter written in Rust (Ru(st) MarkDown Linter)" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/README.md new/rumdl-0.1.95/README.md --- old/rumdl-0.1.94/README.md 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/README.md 2026-05-19 22:15:04.000000000 +0200 @@ -206,7 +206,7 @@ mise install rumdl # Use a specific version for the project -mise use [email protected] +mise use [email protected] ``` ### Using Nix (macOS/Linux) @@ -405,7 +405,7 @@ ```yaml repos: - repo: https://github.com/rvben/rumdl-pre-commit - rev: v0.1.94 + rev: v0.1.95 hooks: - id: rumdl # Lint only (fails on issues) - id: rumdl-fmt # Auto-format and fail if issues remain @@ -427,7 +427,7 @@ ```yaml repos: - repo: https://github.com/rvben/rumdl-pre-commit - rev: v0.1.94 + rev: v0.1.95 hooks: - id: rumdl args: [--no-exclude] # Disable all exclude patterns diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/docs/global-settings.md new/rumdl-0.1.95/docs/global-settings.md --- old/rumdl-0.1.94/docs/global-settings.md 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/docs/global-settings.md 2026-05-19 22:15:04.000000000 +0200 @@ -1342,7 +1342,7 @@ ```yaml - repo: https://github.com/rvben/rumdl-pre-commit - rev: v0.1.94 + rev: v0.1.95 hooks: - id: rumdl args: [--config=.rumdl.toml] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/docs/markdownlint-comparison.md new/rumdl-0.1.95/docs/markdownlint-comparison.md --- old/rumdl-0.1.94/docs/markdownlint-comparison.md 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/docs/markdownlint-comparison.md 2026-05-19 22:15:04.000000000 +0200 @@ -189,6 +189,24 @@ markdownlint does not have built-in flavor support; users must configure individual rules manually. +### 7. Rule-Specific Default Differences + +A few rules ship safer defaults than markdownlint. These are opt-out, not removed - set the documented option to recover markdownlint-exact behavior. + +**MD010 (Hard tabs) - `code_blocks`:** + +- **markdownlint**: defaults `code_blocks: true`, flagging and rewriting tabs inside fenced *and* indented code blocks. +- **rumdl**: defaults `code-blocks = false`, skipping tabs inside both fenced and indented code blocks. +- **Rationale**: tabs are syntactically required in Makefiles and conventional in `gofmt`-formatted Go. markdownlint's default silently corrupts such snippets on auto-fix. Skipping code blocks by default is strictly safer. +- **markdownlint parity**: set `code-blocks = true` to flag tabs everywhere, including code blocks. + +```toml +[MD010] +code-blocks = true # markdownlint-exact behavior +``` + +**Reference:** [rumdl Issue #630](https://github.com/rvben/rumdl/issues/630) - inconsistent tab handling between fenced and indented code blocks. + ## Configuration Compatibility ### Markdownlint Config Auto-Detection diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/docs/md010.md new/rumdl-0.1.95/docs/md010.md --- old/rumdl-0.1.94/docs/md010.md 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/docs/md010.md 2026-05-19 22:15:04.000000000 +0200 @@ -63,12 +63,24 @@ ```toml [MD010] -spaces-per-tab = 4 # Number of spaces to replace each tab with (default: 4) +spaces-per-tab = 4 # Number of spaces to replace each tab with (default: 4) +code-blocks = false # Skip tabs inside code blocks (default: false) ``` ### Configuration options explained -- `spaces-per-tab`: How many spaces to use when replacing each tab character +- `spaces-per-tab`: How many spaces to use when replacing each tab character. +- `code-blocks`: When `false` (default), hard tabs inside fenced and indented + code blocks are skipped - tabs are often required there (Makefiles, Go) and + rewriting them would corrupt examples. Set to `true` for markdownlint-parity + behavior that flags tabs everywhere, including code blocks. + +> **Behavior change:** Earlier versions skipped tabs in fenced code blocks +> while still flagging indented ones. MD010 now treats both code-block types +> consistently and, by default, does not flag tabs in either. Note that a +> document whose content starts with a tab-indented line is a CommonMark +> indented code block, so leading tabs there are no longer flagged by default. +> Set `code-blocks = true` to flag tabs in all code blocks. ## Automatic fixes diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/npm/cli-darwin-arm64/package.json new/rumdl-0.1.95/npm/cli-darwin-arm64/package.json --- old/rumdl-0.1.94/npm/cli-darwin-arm64/package.json 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/npm/cli-darwin-arm64/package.json 2026-05-19 22:15:04.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-darwin-arm64", - "version": "0.1.94", + "version": "0.1.95", "description": "rumdl binary for macOS ARM64 (Apple Silicon)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/npm/cli-darwin-x64/package.json new/rumdl-0.1.95/npm/cli-darwin-x64/package.json --- old/rumdl-0.1.94/npm/cli-darwin-x64/package.json 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/npm/cli-darwin-x64/package.json 2026-05-19 22:15:04.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-darwin-x64", - "version": "0.1.94", + "version": "0.1.95", "description": "rumdl binary for macOS x64 (Intel)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/npm/cli-linux-arm64/package.json new/rumdl-0.1.95/npm/cli-linux-arm64/package.json --- old/rumdl-0.1.94/npm/cli-linux-arm64/package.json 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/npm/cli-linux-arm64/package.json 2026-05-19 22:15:04.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-linux-arm64", - "version": "0.1.94", + "version": "0.1.95", "description": "rumdl binary for Linux ARM64 (glibc)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/npm/cli-linux-arm64-musl/package.json new/rumdl-0.1.95/npm/cli-linux-arm64-musl/package.json --- old/rumdl-0.1.94/npm/cli-linux-arm64-musl/package.json 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/npm/cli-linux-arm64-musl/package.json 2026-05-19 22:15:04.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-linux-arm64-musl", - "version": "0.1.94", + "version": "0.1.95", "description": "rumdl binary for Linux ARM64 (musl/Alpine)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/npm/cli-linux-x64/package.json new/rumdl-0.1.95/npm/cli-linux-x64/package.json --- old/rumdl-0.1.94/npm/cli-linux-x64/package.json 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/npm/cli-linux-x64/package.json 2026-05-19 22:15:04.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-linux-x64", - "version": "0.1.94", + "version": "0.1.95", "description": "rumdl binary for Linux x64 (glibc)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/npm/cli-linux-x64-musl/package.json new/rumdl-0.1.95/npm/cli-linux-x64-musl/package.json --- old/rumdl-0.1.94/npm/cli-linux-x64-musl/package.json 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/npm/cli-linux-x64-musl/package.json 2026-05-19 22:15:04.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-linux-x64-musl", - "version": "0.1.94", + "version": "0.1.95", "description": "rumdl binary for Linux x64 (musl/Alpine)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/npm/cli-win32-x64/package.json new/rumdl-0.1.95/npm/cli-win32-x64/package.json --- old/rumdl-0.1.94/npm/cli-win32-x64/package.json 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/npm/cli-win32-x64/package.json 2026-05-19 22:15:04.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-win32-x64", - "version": "0.1.94", + "version": "0.1.95", "description": "rumdl binary for Windows x64", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/npm/rumdl/package.json new/rumdl-0.1.95/npm/rumdl/package.json --- old/rumdl-0.1.94/npm/rumdl/package.json 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/npm/rumdl/package.json 2026-05-19 22:15:04.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "rumdl", - "version": "0.1.94", + "version": "0.1.95", "description": "A fast Markdown linter written in Rust", "license": "MIT", "repository": { @@ -33,12 +33,12 @@ "node": ">=18.0.0" }, "optionalDependencies": { - "@rumdl/cli-darwin-x64": "0.1.94", - "@rumdl/cli-darwin-arm64": "0.1.94", - "@rumdl/cli-linux-x64": "0.1.94", - "@rumdl/cli-linux-arm64": "0.1.94", - "@rumdl/cli-linux-x64-musl": "0.1.94", - "@rumdl/cli-linux-arm64-musl": "0.1.94", - "@rumdl/cli-win32-x64": "0.1.94" + "@rumdl/cli-darwin-x64": "0.1.95", + "@rumdl/cli-darwin-arm64": "0.1.95", + "@rumdl/cli-linux-x64": "0.1.95", + "@rumdl/cli-linux-arm64": "0.1.95", + "@rumdl/cli-linux-x64-musl": "0.1.95", + "@rumdl/cli-linux-arm64-musl": "0.1.95", + "@rumdl/cli-win32-x64": "0.1.95" } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/src/lsp/tests.rs new/rumdl-0.1.95/src/lsp/tests.rs --- old/rumdl-0.1.94/src/lsp/tests.rs 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/src/lsp/tests.rs 2026-05-19 22:15:04.000000000 +0200 @@ -138,8 +138,10 @@ let server = create_test_server(); let uri = Url::parse("file:///test.md").unwrap(); - // Line 2 and 3 have hard tabs (MD010, fixable), range only covers line 0 - let text = "# Test\n\n\tThis is a test\n\tWith tabs\n"; + // Lines 2 and 3 (0-indexed LSP line) have hard tabs (MD010, fixable), range only covers line 0. + // Tabs are placed mid-line in paragraph text (not at column 0 after a blank line, + // which would be an indented code block skipped by default with code_blocks=false). + let text = "# Test\n\nThis is\ta test\nWith\ttabs\n"; // Range that doesn't cover the violations (line 0 only) let range = Range { @@ -4446,10 +4448,12 @@ let server = create_test_server(); let uri = Url::parse("file:///test.md").unwrap(); - // Line 0: "# Title" -- no fixable issues here - // Line 1: "" -- blank line - // Line 2: "\tTabbed text" -- hard tab (MD010, fixable) - let text = "# Title\n\n\tTabbed text\n"; + // Line 0: "# Title" -- no fixable issues here + // Line 1: "" -- blank line + // Line 2: "Tabbed\ttext" -- hard tab (MD010, fixable), mid-line in paragraph + // Tab placed mid-line to keep it out of an indented code block position + // (column-0 tab after blank line would be skipped with code_blocks=false). + let text = "# Title\n\nTabbed\ttext\n"; // Narrow range: only line 0 (where Zed cursor might be) let narrow_range = Range { @@ -4496,10 +4500,12 @@ let server = create_test_server(); let uri = Url::parse("file:///test.md").unwrap(); - // Two fixable issues on different lines (hard tabs → MD010): - // Line 2: "\tFirst issue" - // Line 4: "\tSecond issue" - let text = "# Title\n\n\tFirst issue\n\n\tSecond issue\n"; + // Two fixable issues on different lines (hard tabs -> MD010). + // Tabs placed mid-line in paragraph text to avoid being treated as + // indented code blocks (column-0 tab after blank line is skipped with code_blocks=false). + // Line 2 (0-indexed LSP line): "First\tissue" + // Line 3 (0-indexed LSP line): "Second\tissue" + let text = "# Title\n\nFirst\tissue\nSecond\tissue\n"; // Range covering only line 2 (first issue) let partial_range = Range { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/src/rules/md010_no_hard_tabs/md010_config.rs new/rumdl-0.1.95/src/rules/md010_no_hard_tabs/md010_config.rs --- old/rumdl-0.1.94/src/rules/md010_no_hard_tabs/md010_config.rs 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/src/rules/md010_no_hard_tabs/md010_config.rs 2026-05-19 22:15:04.000000000 +0200 @@ -9,16 +9,26 @@ /// Number of spaces per tab (default: 4) #[serde(default = "default_spaces_per_tab", alias = "spaces_per_tab")] pub spaces_per_tab: PositiveUsize, + + /// Check for hard tabs inside code blocks (default: false). + /// When false, tabs inside fenced and indented code blocks are skipped. + #[serde(default = "default_code_blocks", alias = "code_blocks")] + pub code_blocks: bool, } fn default_spaces_per_tab() -> PositiveUsize { PositiveUsize::from_const(4) } +fn default_code_blocks() -> bool { + false +} + impl Default for MD010Config { fn default() -> Self { Self { spaces_per_tab: default_spaces_per_tab(), + code_blocks: default_code_blocks(), } } } @@ -66,4 +76,23 @@ let err = result.unwrap_err().to_string(); assert!(err.contains("must be at least 1") || err.contains("got 0")); } + + #[test] + fn test_code_blocks_defaults_false() { + let config = MD010Config::default(); + assert!(!config.code_blocks, "MD010 defaults to skipping code blocks"); + } + + #[test] + fn test_code_blocks_kebab_case() { + let config: MD010Config = toml::from_str("code-blocks = true\n").unwrap(); + assert!(config.code_blocks); + assert_eq!(config.spaces_per_tab.get(), 4); + } + + #[test] + fn test_code_blocks_snake_case_alias() { + let config: MD010Config = toml::from_str("code_blocks = true\n").unwrap(); + assert!(config.code_blocks); + } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/src/rules/md010_no_hard_tabs.rs new/rumdl-0.1.95/src/rules/md010_no_hard_tabs.rs --- old/rumdl-0.1.94/src/rules/md010_no_hard_tabs.rs 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/src/rules/md010_no_hard_tabs.rs 2026-05-19 22:15:04.000000000 +0200 @@ -5,8 +5,8 @@ /// See [docs/md010.md](../../docs/md010.md) for full documentation, configuration, and examples. use crate::utils::range_utils::calculate_match_range; -mod md010_config; -use md010_config::MD010Config; +pub mod md010_config; +pub use md010_config::MD010Config; /// Rule MD010: Hard tabs #[derive(Clone, Default)] @@ -19,6 +19,7 @@ Self { config: MD010Config { spaces_per_tab: crate::types::PositiveUsize::from_const(spaces_per_tab), + code_blocks: false, }, } } @@ -27,52 +28,6 @@ Self { config } } - /// Detect which lines are inside fenced code blocks (``` or ~~~). - /// Only fenced code blocks are skipped — indented code blocks (4+ spaces / tab) - /// are NOT skipped because the tabs themselves are what MD010 should flag. - fn find_fenced_code_block_lines(lines: &[&str]) -> Vec<bool> { - let mut in_fenced_block = false; - let mut fence_char: Option<char> = None; - let mut fence_len: usize = 0; - let mut result = vec![false; lines.len()]; - - for (i, line) in lines.iter().enumerate() { - let trimmed = line.trim_start(); - - if !in_fenced_block { - // Check for opening fence (3+ backticks or tildes) - let first_char = trimmed.chars().next(); - if matches!(first_char, Some('`') | Some('~')) { - let fc = first_char.unwrap(); - let count = trimmed.chars().take_while(|&c| c == fc).count(); - if count >= 3 { - in_fenced_block = true; - fence_char = Some(fc); - fence_len = count; - result[i] = true; - } - } - } else { - result[i] = true; - // Check for closing fence (must match opening fence char and be >= opening length) - if let Some(fc) = fence_char { - let first = trimmed.chars().next(); - if first == Some(fc) { - let count = trimmed.chars().take_while(|&c| c == fc).count(); - // Closing fence must be at least as long as opening, with nothing else on the line - if count >= fence_len && trimmed[count..].trim().is_empty() { - in_fenced_block = false; - fence_char = None; - fence_len = 0; - } - } - } - } - } - - result - } - fn count_leading_tabs(line: &str) -> usize { let mut count = 0; for c in line.chars() { @@ -135,13 +90,12 @@ let mut warnings = Vec::new(); let lines = ctx.raw_lines(); - // Track fenced code blocks separately — we skip FENCED blocks but NOT - // indented code blocks (since tab indentation IS what MD010 should flag) - let fenced_lines = Self::find_fenced_code_block_lines(lines); + // When `code_blocks` is false (the default), skip tabs inside ANY code block - + // fenced and indented alike - using the shared spec-compliant flag. + let skip_code_blocks = !self.config.code_blocks; for (line_num, &line) in lines.iter().enumerate() { - // Skip fenced code blocks (code has its own formatting rules) - if fenced_lines[line_num] { + if skip_code_blocks && ctx.line_info(line_num + 1).is_some_and(|info| info.in_code_block) { continue; } @@ -298,43 +252,101 @@ } #[test] - fn test_leading_tabs() { - let rule = MD010NoHardTabs::default(); + fn test_leading_tabs_skipped_in_indented_code_by_default() { + // Both lines start with a tab at column 0: parsed as an indented code block. + // Default code_blocks=false skips tabs in indented code blocks. let content = "\tIndented line\n\t\tDouble indented"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); - let result = rule.check(&ctx).unwrap(); - assert_eq!(result.len(), 2); - assert_eq!(result[0].line, 1); - assert_eq!(result[0].message, "Found leading tab, use 4 spaces instead"); - assert_eq!(result[1].line, 2); - assert_eq!(result[1].message, "Found 2 leading tabs, use 8 spaces instead"); + + let rule_off = MD010NoHardTabs::default(); + let result_off = rule_off.check(&ctx).unwrap(); + assert!( + result_off.is_empty(), + "indented code block skipped by default, got {result_off:?}" + ); + assert_eq!( + rule_off.fix(&ctx).unwrap(), + "\tIndented line\n\t\tDouble indented", + "fix must preserve indented code block content" + ); + + // code_blocks=true: tabs inside indented code blocks are flagged. + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: crate::types::PositiveUsize::from_const(4), + code_blocks: true, + }); + let result_on = rule_on.check(&ctx).unwrap(); + assert_eq!(result_on.len(), 2, "got {result_on:?}"); + assert_eq!(result_on[0].line, 1); + assert_eq!(result_on[0].message, "Found leading tab, use 4 spaces instead"); + assert_eq!(result_on[1].line, 2); + assert_eq!(result_on[1].message, "Found 2 leading tabs, use 8 spaces instead"); + assert_eq!(rule_on.fix(&ctx).unwrap(), " Indented line\n Double indented"); } #[test] fn test_fix_tabs() { - let rule = MD010NoHardTabs::default(); + // Line 1 starts with a tab at column 0 -> indented code block, skipped by default. + // Line 2 has a mid-line tab (alignment) -> flagged and fixed. let content = "\tIndented\nNormal\tline\nNo tabs"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); - let fixed = rule.fix(&ctx).unwrap(); - assert_eq!(fixed, " Indented\nNormal line\nNo tabs"); + + let rule_off = MD010NoHardTabs::default(); + let warnings_off = rule_off.check(&ctx).unwrap(); + assert_eq!(warnings_off.len(), 1, "got {warnings_off:?}"); + assert_eq!(warnings_off[0].line, 2); + assert_eq!(warnings_off[0].message, "Found tab for alignment, use spaces instead"); + assert_eq!( + rule_off.fix(&ctx).unwrap(), + "\tIndented\nNormal line\nNo tabs", + "indented code block line preserved; alignment tab fixed" + ); + + // code_blocks=true: line 1 is also flagged. + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: crate::types::PositiveUsize::from_const(4), + code_blocks: true, + }); + let warnings_on = rule_on.check(&ctx).unwrap(); + assert_eq!(warnings_on.len(), 2, "got {warnings_on:?}"); + assert_eq!(warnings_on[0].line, 1); + assert_eq!(warnings_on[1].line, 2); + assert_eq!(rule_on.fix(&ctx).unwrap(), " Indented\nNormal line\nNo tabs"); } #[test] fn test_custom_spaces_per_tab() { - let rule = MD010NoHardTabs::new(4); + // Single tab at column 0 -> indented code block, skipped by default. let content = "\tIndented"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); - let fixed = rule.fix(&ctx).unwrap(); - assert_eq!(fixed, " Indented"); + + let rule_off = MD010NoHardTabs::new(4); + assert!( + rule_off.check(&ctx).unwrap().is_empty(), + "indented code block skipped by default" + ); + assert_eq!( + rule_off.fix(&ctx).unwrap(), + "\tIndented", + "indented code block preserved by default" + ); + + // code_blocks=true: tab is flagged and fixed. + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: crate::types::PositiveUsize::from_const(4), + code_blocks: true, + }); + assert_eq!(rule_on.check(&ctx).unwrap().len(), 1); + assert_eq!(rule_on.fix(&ctx).unwrap(), " Indented"); } #[test] - fn test_code_blocks_always_ignored() { + fn test_fenced_code_block_tabs_skipped_by_default() { let rule = MD010NoHardTabs::default(); let content = "Normal\tline\n```\nCode\twith\ttab\n```\nAnother\tline"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); let result = rule.check(&ctx).unwrap(); - // Should only flag tabs outside code blocks - code has its own formatting rules + // By default (code_blocks=false) tabs inside code blocks are skipped assert_eq!(result.len(), 2); assert_eq!(result[0].line, 1); assert_eq!(result[1].line, 5); @@ -344,12 +356,12 @@ } #[test] - fn test_code_blocks_never_checked() { + fn test_fenced_only_content_skipped_by_default() { let rule = MD010NoHardTabs::default(); let content = "```\nCode\twith\ttab\n```"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); let result = rule.check(&ctx).unwrap(); - // Should never flag tabs in code blocks - code has its own formatting rules + // By default (code_blocks=false) tabs in fenced code blocks are skipped // (e.g., Makefiles require tabs, Go uses tabs by convention) assert_eq!(result.len(), 0); } @@ -390,11 +402,32 @@ #[test] fn test_mixed_tabs_and_spaces() { - let rule = MD010NoHardTabs::default(); + // " \t..." (space then tab) and "\t ..." (tab then space): both parsed as + // indented code blocks by the shared spec-compliant flag. + // Default code_blocks=false skips them. let content = " \tMixed indentation\n\t Mixed again"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); - let result = rule.check(&ctx).unwrap(); - assert_eq!(result.len(), 2); + + let rule_off = MD010NoHardTabs::default(); + let result_off = rule_off.check(&ctx).unwrap(); + assert!( + result_off.is_empty(), + "indented code block lines skipped, got {result_off:?}" + ); + assert_eq!( + rule_off.fix(&ctx).unwrap(), + " \tMixed indentation\n\t Mixed again", + "content preserved unchanged" + ); + + // code_blocks=true: both lines flagged. + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: crate::types::PositiveUsize::from_const(4), + code_blocks: true, + }); + let result_on = rule_on.check(&ctx).unwrap(); + assert_eq!(result_on.len(), 2, "got {result_on:?}"); + assert_eq!(rule_on.fix(&ctx).unwrap(), " Mixed indentation\n Mixed again"); } #[test] @@ -447,20 +480,42 @@ #[test] fn test_from_config() { - // Test that custom config values are properly loaded - let custom_spaces = 8; - let rule = MD010NoHardTabs::new(custom_spaces); - let content = "\tTab"; - let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); - let fixed = rule.fix(&ctx).unwrap(); - assert_eq!(fixed, " Tab"); + // "\tTab" at column 0 -> indented code block, skipped by default (code_blocks=false). + let content_plain = "\tTab"; + let ctx_plain = LintContext::new(content_plain, crate::config::MarkdownFlavor::Standard, None); + let rule_8_off = MD010NoHardTabs::new(8); // spaces_per_tab=8, code_blocks=false + assert!( + rule_8_off.check(&ctx_plain).unwrap().is_empty(), + "indented code block skipped" + ); + assert_eq!( + rule_8_off.fix(&ctx_plain).unwrap(), + "\tTab", + "content preserved unchanged" + ); - // Code blocks are always ignored - let content_with_code = "```\n\tTab in code\n```"; - let ctx = LintContext::new(content_with_code, crate::config::MarkdownFlavor::Standard, None); - let result = rule.check(&ctx).unwrap(); - // Tabs in code blocks are never flagged - assert!(result.is_empty()); + // code_blocks=true: the tab is flagged and replaced with 8 spaces. + let rule_8_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: crate::types::PositiveUsize::from_const(8), + code_blocks: true, + }); + assert_eq!(rule_8_on.check(&ctx_plain).unwrap().len(), 1); + assert_eq!(rule_8_on.fix(&ctx_plain).unwrap(), " Tab"); + + // Fenced code block: tab skipped by default. + let content_fenced = "```\n\tTab in code\n```"; + let ctx_fenced = LintContext::new(content_fenced, crate::config::MarkdownFlavor::Standard, None); + assert!( + rule_8_off.check(&ctx_fenced).unwrap().is_empty(), + "fenced code block skipped" + ); + assert_eq!(rule_8_off.fix(&ctx_fenced).unwrap(), "```\n\tTab in code\n```"); + + // code_blocks=true: tab inside fence is flagged. + let result_on = rule_8_on.check(&ctx_fenced).unwrap(); + assert_eq!(result_on.len(), 1, "got {result_on:?}"); + assert_eq!(result_on[0].line, 2); + assert_eq!(rule_8_on.fix(&ctx_fenced).unwrap(), "```\n Tab in code\n```"); } #[test] @@ -503,14 +558,14 @@ } #[test] - fn test_code_blocks_always_preserved_in_fix() { + fn test_fenced_code_block_tabs_preserved_in_fix_by_default() { let rule = MD010NoHardTabs::default(); let content = "Text\twith\ttab\n```makefile\ntarget:\n\tcommand\n\tanother\n```\nMore\ttabs"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); let fixed = rule.fix(&ctx).unwrap(); - // Tabs in code blocks are preserved - code has its own formatting rules + // By default (code_blocks=false) tabs in fenced code blocks are preserved // (e.g., Makefiles require tabs, Go uses tabs by convention) let expected = "Text with tab\n```makefile\ntarget:\n\tcommand\n\tanother\n```\nMore tabs"; assert_eq!(fixed, expected); @@ -554,23 +609,40 @@ } #[test] - fn test_indented_code_block_tabs_flagged() { - let rule = MD010NoHardTabs::default(); - // Tabs in indented code blocks are flagged because the tab IS the problem - // (unlike fenced code blocks where tabs are part of the code formatting) + fn test_indented_code_block_tabs_skipped_by_default() { + // " code\twith\ttab" is indented with 4 spaces -> indented code block. + // Default code_blocks=false skips it; only the tab on the normal line is flagged. let content = " code\twith\ttab\n\nNormal\ttext"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); - let result = rule.check(&ctx).unwrap(); + + let rule_off = MD010NoHardTabs::default(); + let result_off = rule_off.check(&ctx).unwrap(); assert_eq!( - result.len(), + result_off.len(), + 1, + "expected 1 warning (only normal-text tab), got {}: {:?}", + result_off.len(), + result_off + ); + assert_eq!(result_off[0].line, 3); + assert_eq!(result_off[0].message, "Found tab for alignment, use spaces instead"); + + // code_blocks=true: all 3 tabs flagged (2 on line 1, 1 on line 3). + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: crate::types::PositiveUsize::from_const(4), + code_blocks: true, + }); + let result_on = rule_on.check(&ctx).unwrap(); + assert_eq!( + result_on.len(), 3, - "Expected 3 warnings but got {}: {:?}", - result.len(), - result + "expected 3 warnings with code_blocks=true, got {}: {:?}", + result_on.len(), + result_on ); - assert_eq!(result[0].line, 1); - assert_eq!(result[1].line, 1); - assert_eq!(result[2].line, 3); + assert_eq!(result_on[0].line, 1); + assert_eq!(result_on[1].line, 1); + assert_eq!(result_on[2].line, 3); } #[test] @@ -601,11 +673,98 @@ #[test] fn test_fix_indented_code_block_tabs_replaced() { - let rule = MD010NoHardTabs::default(); + // Default code_blocks=false: indented code block tabs preserved, normal-text tab fixed. let content = " code\twith\ttab\n\nNormal\ttext"; let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); - let fixed = rule.fix(&ctx).unwrap(); - // All tabs replaced, including those in indented code blocks - assert_eq!(fixed, " code with tab\n\nNormal text"); + + let rule_off = MD010NoHardTabs::default(); + assert_eq!( + rule_off.fix(&ctx).unwrap(), + " code\twith\ttab\n\nNormal text", + "indented code block preserved; only normal-text tab fixed" + ); + + // code_blocks=true: all tabs replaced including those in the indented code block. + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: crate::types::PositiveUsize::from_const(4), + code_blocks: true, + }); + assert_eq!( + rule_on.fix(&ctx).unwrap(), + " code with tab\n\nNormal text", + "all tabs replaced with code_blocks=true" + ); + } + + #[test] + fn test_issue_630_default_skips_both_code_blocks() { + // Default code_blocks = false: tabs skipped in BOTH block types. + let rule = MD010NoHardTabs::default(); + 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"; + let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!(result.is_empty(), "both code blocks skipped, got {result:?}"); + } + + #[test] + fn test_issue_630_code_blocks_true_flags_both() { + // code_blocks = true: tabs flagged in BOTH block types. + let rule = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: crate::types::PositiveUsize::from_const(4), + code_blocks: true, + }); + 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"; + let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + // Line 4 " \tfoo()": one alignment tab group inside the indented block. + // Line 11 "\tfoo()": one leading tab group inside the fenced block. + assert_eq!(result.len(), 2, "got {result:?}"); + assert_eq!(result[0].line, 4); + assert_eq!(result[1].line, 11); + } + + #[test] + fn test_code_blocks_toggle_fenced() { + let content = "Normal\tline\n```\nCode\twith\ttab\n```\nAnother\tline"; + + // Default false: only the two tab groups outside the fence. + let off = MD010NoHardTabs::default(); + let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); + let r_off = off.check(&ctx).unwrap(); + assert_eq!(r_off.len(), 2, "got {r_off:?}"); + assert_eq!(r_off[0].line, 1); + assert_eq!(r_off[1].line, 5); + assert_eq!( + off.fix(&ctx).unwrap(), + "Normal line\n```\nCode\twith\ttab\n```\nAnother line" + ); + + // true: also the two groups on the fenced content line. + let on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: crate::types::PositiveUsize::from_const(4), + code_blocks: true, + }); + let r_on = on.check(&ctx).unwrap(); + assert_eq!(r_on.len(), 4, "got {r_on:?}"); + assert_eq!(r_on[0].line, 1); + assert_eq!(r_on[1].line, 3); + assert_eq!(r_on[2].line, 3); + assert_eq!(r_on[3].line, 5); + assert_eq!( + on.fix(&ctx).unwrap(), + "Normal line\n```\nCode with tab\n```\nAnother line" + ); + } + + #[test] + fn test_code_blocks_toggle_makefile_fence_preserved_by_default() { + let content = "Text\twith\ttab\n```makefile\ntarget:\n\tcommand\n```\nMore\ttabs"; + let off = MD010NoHardTabs::default(); + let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); + // Default preserves the Makefile recipe tab; only prose tabs fixed. + assert_eq!( + off.fix(&ctx).unwrap(), + "Text with tab\n```makefile\ntarget:\n\tcommand\n```\nMore tabs" + ); } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/src/rules/mod.rs new/rumdl-0.1.95/src/rules/mod.rs --- old/rumdl-0.1.94/src/rules/mod.rs 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/src/rules/mod.rs 2026-05-19 22:15:04.000000000 +0200 @@ -10,7 +10,7 @@ mod md005_list_indent; pub mod md007_ul_indent; mod md009_trailing_spaces; -mod md010_no_hard_tabs; +pub mod md010_no_hard_tabs; mod md011_no_reversed_links; pub mod md013_line_length; mod md014_commands_show_output; @@ -79,7 +79,7 @@ pub use md005_list_indent::MD005ListIndent; pub use md007_ul_indent::MD007ULIndent; pub use md009_trailing_spaces::MD009TrailingSpaces; -pub use md010_no_hard_tabs::MD010NoHardTabs; +pub use md010_no_hard_tabs::{MD010Config, MD010NoHardTabs}; pub use md011_no_reversed_links::MD011NoReversedLinks; pub use md013_line_length::MD013Config; pub use md013_line_length::MD013LineLength; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/tests/formats/mkdocs_extensions_test.rs new/rumdl-0.1.95/tests/formats/mkdocs_extensions_test.rs --- old/rumdl-0.1.94/tests/formats/mkdocs_extensions_test.rs 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/tests/formats/mkdocs_extensions_test.rs 2026-05-19 22:15:04.000000000 +0200 @@ -832,7 +832,9 @@ #[test] fn test_hard_tabs_detected() { - let content = "# Test\n\n\tIndented with tab.\n"; + // Use a tab mid-line in a paragraph (not at column 0 after a blank line, + // which would be an indented code block skipped by default). + let content = "# Test\n\nParagraph with\ta hard tab.\n"; let warnings = lint_mkdocs(content); let md010 = warnings .iter() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.94/tests/rules/md010_test.rs new/rumdl-0.1.95/tests/rules/md010_test.rs --- old/rumdl-0.1.94/tests/rules/md010_test.rs 2026-05-18 12:11:42.000000000 +0200 +++ new/rumdl-0.1.95/tests/rules/md010_test.rs 2026-05-19 22:15:04.000000000 +0200 @@ -1,6 +1,7 @@ use rumdl_lib::lint_context::LintContext; use rumdl_lib::rule::Rule; -use rumdl_lib::rules::MD010NoHardTabs; +use rumdl_lib::rules::{MD010Config, MD010NoHardTabs}; +use rumdl_lib::types::PositiveUsize; #[test] fn test_no_hard_tabs() { @@ -24,15 +25,35 @@ #[test] fn test_leading_hard_tabs() { - let rule = MD010NoHardTabs::default(); + // Both lines start with a tab at column 0 -> indented code block. + // Default code_blocks=false skips tabs in indented code blocks. let content = "\tIndented line\n\t\tDouble indented"; let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); - let result = rule.check(&ctx).unwrap(); - assert_eq!(result.len(), 2); // One warning per line (grouped consecutive tabs) - assert_eq!(result[0].line, 1); - assert_eq!(result[0].message, "Found leading tab, use 4 spaces instead"); - assert_eq!(result[1].line, 2); - assert_eq!(result[1].message, "Found 2 leading tabs, use 8 spaces instead"); + + let rule_off = MD010NoHardTabs::default(); + let result_off = rule_off.check(&ctx).unwrap(); + assert!( + result_off.is_empty(), + "indented code block skipped by default, got {result_off:?}" + ); + assert_eq!( + rule_off.fix(&ctx).unwrap(), + "\tIndented line\n\t\tDouble indented", + "content preserved unchanged" + ); + + // code_blocks=true: tabs in indented code blocks are flagged. + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: PositiveUsize::from_const(4), + code_blocks: true, + }); + let result_on = rule_on.check(&ctx).unwrap(); + assert_eq!(result_on.len(), 2, "got {result_on:?}"); + assert_eq!(result_on[0].line, 1); + assert_eq!(result_on[0].message, "Found leading tab, use 4 spaces instead"); + assert_eq!(result_on[1].line, 2); + assert_eq!(result_on[1].message, "Found 2 leading tabs, use 8 spaces instead"); + assert_eq!(rule_on.fix(&ctx).unwrap(), " Indented line\n Double indented"); } #[test] @@ -48,20 +69,36 @@ #[test] fn test_empty_line_tabs() { - let rule = MD010NoHardTabs::default(); + // Line 2 "\t\t" is an empty line with tabs -> always flagged (not an indented code block). + // Line 3 "\tMore text" starts with tab at column 0 -> indented code block, skipped by default. let content = "Normal line\n\t\t\n\tMore text"; let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); - let result = rule.check(&ctx).unwrap(); - // Tab-indented content is flagged (might be accidental) - assert_eq!(result.len(), 2); // Empty line with tabs + tab on line 3 - assert_eq!(result[0].line, 2); - assert_eq!(result[0].message, "Empty line contains 2 tabs"); - assert_eq!(result[1].line, 3); - assert_eq!(result[1].message, "Found leading tab, use 4 spaces instead"); + + let rule_off = MD010NoHardTabs::default(); + let result_off = rule_off.check(&ctx).unwrap(); + assert_eq!(result_off.len(), 1, "only empty-line tabs flagged, got {result_off:?}"); + assert_eq!(result_off[0].line, 2); + assert_eq!(result_off[0].message, "Empty line contains 2 tabs"); + // Empty-line tabs replaced; indented code block line preserved. + assert_eq!(rule_off.fix(&ctx).unwrap(), "Normal line\n \n\tMore text"); + + // code_blocks=true: line 3 indented code block tab also flagged. + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: PositiveUsize::from_const(4), + code_blocks: true, + }); + let result_on = rule_on.check(&ctx).unwrap(); + assert_eq!(result_on.len(), 2, "got {result_on:?}"); + assert_eq!(result_on[0].line, 2); + assert_eq!(result_on[0].message, "Empty line contains 2 tabs"); + assert_eq!(result_on[1].line, 3); + assert_eq!(result_on[1].message, "Found leading tab, use 4 spaces instead"); + assert_eq!(rule_on.fix(&ctx).unwrap(), "Normal line\n \n More text"); } #[test] fn test_code_blocks_allowed() { + // Intentionally mirrors test_code_blocks_not_allowed; do not delete the redundancy. let rule = MD010NoHardTabs::new(4); let content = "Normal line\n```\n\tCode with tab\n\tMore code\n```\nNormal\tline"; let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); @@ -72,41 +109,124 @@ #[test] fn test_code_blocks_not_allowed() { - let rule = MD010NoHardTabs::default(); // code blocks are always skipped now + // Fenced code block tabs are skipped by default (flagged when code_blocks=true). let content = "Normal line\n```\n\tCode with tab\n\tMore code\n```\nNormal\tline"; let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); - let result = rule.check(&ctx).unwrap(); - assert_eq!(result.len(), 1); // Only tab outside code block is flagged - assert_eq!(result[0].line, 6); + + let rule_off = MD010NoHardTabs::default(); + let result_off = rule_off.check(&ctx).unwrap(); + assert_eq!(result_off.len(), 1); // Only tab outside code block is flagged + assert_eq!(result_off[0].line, 6); + + // code_blocks=true: tabs inside the fenced block are also flagged. + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: PositiveUsize::from_const(4), + code_blocks: true, + }); + let result_on = rule_on.check(&ctx).unwrap(); + assert_eq!(result_on.len(), 3, "got {result_on:?}"); + assert_eq!(result_on[0].line, 3); + assert_eq!(result_on[0].message, "Found leading tab, use 4 spaces instead"); + assert_eq!(result_on[1].line, 4); + assert_eq!(result_on[1].message, "Found leading tab, use 4 spaces instead"); + assert_eq!(result_on[2].line, 6); + assert_eq!(result_on[2].message, "Found tab for alignment, use spaces instead"); + assert_eq!( + rule_on.fix(&ctx).unwrap(), + "Normal line\n```\n Code with tab\n More code\n```\nNormal line" + ); } #[test] fn test_fix_with_code_blocks() { - let rule = MD010NoHardTabs::new(2); // 2 spaces per tab, preserve code blocks + // Default code_blocks=false: lines 1 and 5 are indented code blocks (tab at column 0); + // line 3 is inside a fenced code block. All tabs skipped -> content preserved as-is. let content = "\tIndented line\n```\n\tCode\n```\n\t\tDouble indented"; let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); - let fixed = rule.fix(&ctx).unwrap(); - assert_eq!(fixed, " Indented line\n```\n\tCode\n```\n Double indented"); + + let rule_off = MD010NoHardTabs::new(2); + assert!( + rule_off.check(&ctx).unwrap().is_empty(), + "all tabs in code blocks skipped" + ); + assert_eq!( + rule_off.fix(&ctx).unwrap(), + "\tIndented line\n```\n\tCode\n```\n\t\tDouble indented", + "content preserved unchanged" + ); +} + +#[test] +fn test_fix_with_code_blocks_true_variant() { + // code_blocks=true: tabs in both fenced and indented code blocks are replaced. + let content = "\tIndented line\n```\n\tCode\n```\n\t\tDouble indented"; + let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); + + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: PositiveUsize::from_const(2), + code_blocks: true, + }); + let result_on = rule_on.check(&ctx).unwrap(); + assert_eq!(result_on.len(), 3, "got {result_on:?}"); + assert_eq!(result_on[0].line, 1); + assert_eq!(result_on[1].line, 3); + assert_eq!(result_on[2].line, 5); + assert_eq!( + rule_on.fix(&ctx).unwrap(), + " Indented line\n```\n Code\n```\n Double indented" + ); } #[test] fn test_fix_without_code_blocks() { - let rule = MD010NoHardTabs::new(2); // 2 spaces per tab, code blocks always preserved + // Intentionally duplicates test_fix_with_code_blocks content as a historical regression + // counterpart; do not merge or delete either. The code_blocks=true behavior for this + // content lives in test_fix_with_code_blocks_true_variant. let content = "\tIndented line\n```\n\tCode\n```\n\t\tDouble indented"; let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); - let fixed = rule.fix(&ctx).unwrap(); - assert_eq!(fixed, " Indented line\n```\n\tCode\n```\n Double indented"); + + let rule_off = MD010NoHardTabs::new(2); + assert!( + rule_off.check(&ctx).unwrap().is_empty(), + "all tabs in code blocks skipped" + ); + assert_eq!( + rule_off.fix(&ctx).unwrap(), + "\tIndented line\n```\n\tCode\n```\n\t\tDouble indented", + "content preserved unchanged" + ); } #[test] fn test_mixed_indentation() { - let rule = MD010NoHardTabs::default(); + // " Spaces" is space-indented (not a tab). "\tTab" and " \tMixed" start with + // tab/space-tab and are classified as indented code blocks. + // Default code_blocks=false skips indented code blocks. let content = " Spaces\n\tTab\n \tMixed"; let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); - let result = rule.check(&ctx).unwrap(); - assert_eq!(result.len(), 2); - assert_eq!(result[0].line, 2); - assert_eq!(result[1].line, 3); + + let rule_off = MD010NoHardTabs::default(); + let result_off = rule_off.check(&ctx).unwrap(); + assert!( + result_off.is_empty(), + "indented code block lines skipped, got {result_off:?}" + ); + assert_eq!( + rule_off.fix(&ctx).unwrap(), + " Spaces\n\tTab\n \tMixed", + "content preserved unchanged" + ); + + // code_blocks=true: tabs on lines 2 and 3 are flagged. + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: PositiveUsize::from_const(4), + code_blocks: true, + }); + let result_on = rule_on.check(&ctx).unwrap(); + assert_eq!(result_on.len(), 2, "got {result_on:?}"); + assert_eq!(result_on[0].line, 2); + assert_eq!(result_on[1].line, 3); + assert_eq!(rule_on.fix(&ctx).unwrap(), " Spaces\n Tab\n Mixed"); } #[test] @@ -164,22 +284,45 @@ #[test] fn test_md010_tabs_in_indented_code() { - let rule = MD010NoHardTabs::new(4); - - // Tab-indented content is flagged as it might be accidental - // (even if it looks like an indented code block) + // Lines 3-4 start with double-tabs -> indented code block. + // Default code_blocks=false skips indented code blocks; line 6 alignment tabs fixed. let content = "Text\n\n\t\tCode with tabs\n\t\tMore code\n\nText\twith\ttab"; let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); - let fixed = rule.fix(&ctx).unwrap(); - // Tab-indented content is converted to spaces (8 spaces = 2 tabs * 4 spaces) + let rule_off = MD010NoHardTabs::new(4); + let result_off = rule_off.check(&ctx).unwrap(); + assert_eq!(result_off.len(), 2, "got {result_off:?}"); + assert_eq!(result_off[0].line, 6); + assert_eq!(result_off[1].line, 6); + let fixed_off = rule_off.fix(&ctx).unwrap(); assert!( - fixed.contains(" Code with tabs"), - "Tabs in tab-indented content should be replaced" + fixed_off.contains("\t\tCode with tabs"), + "indented code block preserved, got: {fixed_off:?}" ); assert!( - fixed.contains("Text with tab"), - "Tabs outside code should be replaced" + fixed_off.contains("Text with tab"), + "alignment tabs fixed, got: {fixed_off:?}" + ); + + // code_blocks=true: indented code block tabs also flagged and replaced. + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: PositiveUsize::from_const(4), + code_blocks: true, + }); + let result_on = rule_on.check(&ctx).unwrap(); + assert_eq!(result_on.len(), 4, "got {result_on:?}"); + assert_eq!(result_on[0].line, 3); + assert_eq!(result_on[1].line, 4); + assert_eq!(result_on[2].line, 6); + assert_eq!(result_on[3].line, 6); + let fixed_on = rule_on.fix(&ctx).unwrap(); + assert!( + fixed_on.contains(" Code with tabs"), + "indented code tabs replaced (2 tabs * 4 spaces), got: {fixed_on:?}" + ); + assert!( + fixed_on.contains("Text with tab"), + "alignment tabs fixed, got: {fixed_on:?}" ); } @@ -249,38 +392,73 @@ #[test] fn test_tab_character_in_different_positions() { - let rule = MD010NoHardTabs::default(); - - // Test tabs at start, middle, and end + // Line 1 "\tStart tab" starts with a tab at column 0 -> indented code block, skipped by default. + // Lines 2-5 have non-leading or leading tabs on non-code-block lines. let content = "\tStart tab\nMiddle\ttab\nEnd tab\t\n\t\tDouble start\nMixed \t \t spaces"; let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); - let result = rule.check(&ctx).unwrap(); - assert_eq!(result.len(), 6, "Should detect all tabs"); - assert_eq!(result[0].message, "Found leading tab, use 4 spaces instead"); - assert_eq!(result[1].message, "Found tab for alignment, use spaces instead"); - assert_eq!(result[2].message, "Found tab for alignment, use spaces instead"); - assert_eq!(result[3].message, "Found 2 leading tabs, use 8 spaces instead"); - assert_eq!(result[4].message, "Found tab for alignment, use spaces instead"); - assert_eq!(result[5].message, "Found tab for alignment, use spaces instead"); + let rule_off = MD010NoHardTabs::default(); + let result_off = rule_off.check(&ctx).unwrap(); + assert_eq!(result_off.len(), 5, "got {result_off:?}"); + assert_eq!(result_off[0].line, 2); + assert_eq!(result_off[0].message, "Found tab for alignment, use spaces instead"); + assert_eq!(result_off[1].line, 3); + assert_eq!(result_off[1].message, "Found tab for alignment, use spaces instead"); + assert_eq!(result_off[2].line, 4); + assert_eq!(result_off[2].message, "Found 2 leading tabs, use 8 spaces instead"); + assert_eq!(result_off[3].line, 5); + assert_eq!(result_off[3].message, "Found tab for alignment, use spaces instead"); + assert_eq!(result_off[4].line, 5); + assert_eq!(result_off[4].message, "Found tab for alignment, use spaces instead"); + assert_eq!( + rule_off.fix(&ctx).unwrap(), + "\tStart tab\nMiddle tab\nEnd tab \n Double start\nMixed spaces" + ); + + // code_blocks=true: line 1 is also flagged. + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: PositiveUsize::from_const(4), + code_blocks: true, + }); + let result_on = rule_on.check(&ctx).unwrap(); + assert_eq!(result_on.len(), 6, "got {result_on:?}"); + assert_eq!(result_on[0].line, 1); + assert_eq!(result_on[0].message, "Found leading tab, use 4 spaces instead"); + assert_eq!( + rule_on.fix(&ctx).unwrap(), + " Start tab\nMiddle tab\nEnd tab \n Double start\nMixed spaces" + ); } #[test] fn test_mixed_tabs_and_spaces_detailed() { - let rule = MD010NoHardTabs::default(); - - // Various mixed indentation patterns + // All four lines have tabs mixed with leading spaces -> all classified as indented code blocks. + // Default code_blocks=false skips all of them. let content = " \tTwo spaces then tab\n\t Tab then two spaces\n \t \t Space tab space tab\n\t\t Two tabs then spaces"; let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); - let result = rule.check(&ctx).unwrap(); - assert_eq!(result.len(), 5, "Should detect all tabs"); + let rule_off = MD010NoHardTabs::default(); + let result_off = rule_off.check(&ctx).unwrap(); + assert!( + result_off.is_empty(), + "all lines are indented code blocks, got {result_off:?}" + ); + assert_eq!( + rule_off.fix(&ctx).unwrap(), + " \tTwo spaces then tab\n\t Tab then two spaces\n \t \t Space tab space tab\n\t\t Two tabs then spaces", + "content preserved unchanged" + ); - // Fix test - let fixed = rule.fix(&ctx).unwrap(); + // code_blocks=true: 5 tab groups flagged and fixed across all lines. + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: PositiveUsize::from_const(4), + code_blocks: true, + }); + let result_on = rule_on.check(&ctx).unwrap(); + assert_eq!(result_on.len(), 5, "got {result_on:?}"); assert_eq!( - fixed, + rule_on.fix(&ctx).unwrap(), " Two spaces then tab\n Tab then two spaces\n Space tab space tab\n Two tabs then spaces" ); } @@ -308,20 +486,40 @@ #[test] fn test_configuration_spaces_per_tab() { - // Test different spaces_per_tab configurations + // All lines start with tabs at column 0 -> indented code block. + // Default code_blocks=false skips them regardless of spaces_per_tab. let content = "\tOne tab\n\t\tTwo tabs\n\t\t\tThree tabs"; - - // Test with 2 spaces per tab - let rule2 = MD010NoHardTabs::new(2); let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); - let fixed2 = rule2.fix(&ctx).unwrap(); - assert_eq!(fixed2, " One tab\n Two tabs\n Three tabs"); - // Test with 8 spaces per tab - let rule8 = MD010NoHardTabs::new(8); - let fixed8 = rule8.fix(&ctx).unwrap(); + let rule2_off = MD010NoHardTabs::new(2); + assert!(rule2_off.check(&ctx).unwrap().is_empty(), "indented code block skipped"); + assert_eq!( + rule2_off.fix(&ctx).unwrap(), + "\tOne tab\n\t\tTwo tabs\n\t\t\tThree tabs", + "content preserved with spaces_per_tab=2" + ); + + let rule8_off = MD010NoHardTabs::new(8); + assert!(rule8_off.check(&ctx).unwrap().is_empty(), "indented code block skipped"); assert_eq!( - fixed8, + rule8_off.fix(&ctx).unwrap(), + "\tOne tab\n\t\tTwo tabs\n\t\t\tThree tabs", + "content preserved with spaces_per_tab=8" + ); + + // code_blocks=true: spaces_per_tab controls substitution width. + let rule2_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: PositiveUsize::from_const(2), + code_blocks: true, + }); + assert_eq!(rule2_on.fix(&ctx).unwrap(), " One tab\n Two tabs\n Three tabs"); + + let rule8_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: PositiveUsize::from_const(8), + code_blocks: true, + }); + assert_eq!( + rule8_on.fix(&ctx).unwrap(), " One tab\n Two tabs\n Three tabs" ); } @@ -329,63 +527,122 @@ #[test] fn test_configuration_code_blocks_parameter() { let content = "Normal\ttab\n\n```javascript\nfunction\tfoo() {\n\treturn\ttrue;\n}\n```\n\nAnother\ttab"; - - // Code blocks are always skipped now - let rule = MD010NoHardTabs::new(4); let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); - let result = rule.check(&ctx).unwrap(); - assert_eq!(result.len(), 2, "Should always skip tabs in code blocks"); - assert_eq!(result[0].line, 1); - assert_eq!(result[1].line, 9); - // Verify fix behavior - let fixed = rule.fix(&ctx).unwrap(); - assert!(fixed.contains("function\tfoo()"), "Should preserve tabs in code blocks"); - assert!(fixed.contains("Normal tab"), "Should fix tabs outside code blocks"); + // Default code_blocks=false: only tabs outside the fenced block are flagged. + let rule_off = MD010NoHardTabs::new(4); + let result_off = rule_off.check(&ctx).unwrap(); + assert_eq!(result_off.len(), 2, "only prose tabs flagged, got {result_off:?}"); + assert_eq!(result_off[0].line, 1); + assert_eq!(result_off[1].line, 9); + let fixed_off = rule_off.fix(&ctx).unwrap(); + assert!(fixed_off.contains("function\tfoo()"), "fenced code block preserved"); + assert!(fixed_off.contains("Normal tab"), "prose tab fixed"); + assert_eq!( + fixed_off, + "Normal tab\n\n```javascript\nfunction\tfoo() {\n\treturn\ttrue;\n}\n```\n\nAnother tab" + ); + + // code_blocks=true: tabs inside the fenced block are also flagged. + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: PositiveUsize::from_const(4), + code_blocks: true, + }); + let result_on = rule_on.check(&ctx).unwrap(); + assert_eq!(result_on.len(), 5, "got {result_on:?}"); + assert_eq!(result_on[0].line, 1); + assert_eq!(result_on[1].line, 4); + assert_eq!(result_on[2].line, 5); + assert_eq!(result_on[3].line, 5); + assert_eq!(result_on[4].line, 9); + assert_eq!( + rule_on.fix(&ctx).unwrap(), + "Normal tab\n\n```javascript\nfunction foo() {\n return true;\n}\n```\n\nAnother tab" + ); } #[test] fn test_consecutive_vs_separate_tabs() { - let rule = MD010NoHardTabs::default(); - - // Test grouping of consecutive tabs + // Line 1 "\t\t\tThree consecutive" starts with 3 tabs at column 0 -> indented code block. + // Default code_blocks=false skips it; line 2 alignment tabs are flagged (3 separate groups). let content = "\t\t\tThree consecutive\nOne\tthen\tanother\t"; let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); - let result = rule.check(&ctx).unwrap(); - assert_eq!(result.len(), 4, "Should have 1 group for consecutive, 3 separate"); - assert_eq!(result[0].message, "Found 3 leading tabs, use 12 spaces instead"); - assert_eq!(result[1].message, "Found tab for alignment, use spaces instead"); - assert_eq!(result[2].message, "Found tab for alignment, use spaces instead"); - assert_eq!(result[3].message, "Found tab for alignment, use spaces instead"); + let rule_off = MD010NoHardTabs::default(); + let result_off = rule_off.check(&ctx).unwrap(); + assert_eq!( + result_off.len(), + 3, + "3 separate alignment tab groups on line 2, got {result_off:?}" + ); + assert_eq!(result_off[0].line, 2); + assert_eq!(result_off[0].message, "Found tab for alignment, use spaces instead"); + assert_eq!(result_off[1].line, 2); + assert_eq!(result_off[1].message, "Found tab for alignment, use spaces instead"); + assert_eq!(result_off[2].line, 2); + assert_eq!(result_off[2].message, "Found tab for alignment, use spaces instead"); + assert_eq!( + rule_off.fix(&ctx).unwrap(), + "\t\t\tThree consecutive\nOne then another " + ); + + // code_blocks=true: line 1 consecutive-tab group is also flagged. + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: PositiveUsize::from_const(4), + code_blocks: true, + }); + let result_on = rule_on.check(&ctx).unwrap(); + assert_eq!(result_on.len(), 4, "got {result_on:?}"); + assert_eq!(result_on[0].line, 1); + assert_eq!(result_on[0].message, "Found 3 leading tabs, use 12 spaces instead"); + assert_eq!( + rule_on.fix(&ctx).unwrap(), + " Three consecutive\nOne then another " + ); } #[test] fn test_fix_preserves_content_structure() { - let rule = MD010NoHardTabs::default(); - - // Complex content with various elements let content = "# Header\n\n\tIndented paragraph\n\n- List\n\t- Nested\n\t\t- Double nested\n\n```\n\tCode block\n```\n\n> Quote\n> \tWith tab\n\n| Col1\t| Col2\t|\n|---\t|---\t|\n| Data\t| Data\t|"; - let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None); - let fixed = rule.fix(&ctx).unwrap(); - // Verify structure is preserved - assert!(fixed.contains("# Header"), "Headers preserved"); - // Tab-indented content is converted to spaces (might be accidental) - assert!( - fixed.contains(" Indented paragraph"), - "Tab-indented content converted" - ); - assert!(fixed.contains(" - Nested"), "List indentation converted"); - assert!( - fixed.contains(" - Double nested"), - "Double indentation converted" - ); - // Fenced code blocks are preserved - assert!(fixed.contains("\tCode block"), "Code block tabs preserved"); - assert!(fixed.contains("> With tab"), "Quote tab converted"); - assert!(fixed.contains("| Col1 | Col2 |"), "Table tabs converted"); + // Default code_blocks=false: indented paragraph (line 3) and code block content (line 10) + // are preserved; list item tabs and table/quote tabs outside code blocks are fixed. + let rule_off = MD010NoHardTabs::default(); + let fixed_off = rule_off.fix(&ctx).unwrap(); + assert!(fixed_off.contains("# Header"), "headers preserved"); + assert!( + fixed_off.contains("\tIndented paragraph"), + "indented code block preserved, got: {fixed_off:?}" + ); + assert!(fixed_off.contains(" - Nested"), "list indentation converted"); + assert!( + fixed_off.contains(" - Double nested"), + "double list indentation converted" + ); + assert!(fixed_off.contains("\tCode block"), "fenced code block tab preserved"); + assert!(fixed_off.contains("> With tab"), "quote tab converted"); + assert!(fixed_off.contains("| Col1 | Col2 |"), "table tabs converted"); + assert_eq!( + fixed_off, + "# Header\n\n\tIndented paragraph\n\n- List\n - Nested\n - Double nested\n\n```\n\tCode block\n```\n\n> Quote\n> With tab\n\n| Col1 | Col2 |\n|--- |--- |\n| Data | Data |" + ); + + // code_blocks=true: indented paragraph and fenced code block tabs are also fixed. + let rule_on = MD010NoHardTabs::from_config_struct(MD010Config { + spaces_per_tab: PositiveUsize::from_const(4), + code_blocks: true, + }); + let fixed_on = rule_on.fix(&ctx).unwrap(); + assert!( + fixed_on.contains(" Indented paragraph"), + "indented paragraph converted with code_blocks=true, got: {fixed_on:?}" + ); + assert!(fixed_on.contains(" Code block"), "fenced code block tab converted"); + assert_eq!( + fixed_on, + "# Header\n\n Indented paragraph\n\n- List\n - Nested\n - Double nested\n\n```\n Code block\n```\n\n> Quote\n> With tab\n\n| Col1 | Col2 |\n|--- |--- |\n| Data | Data |" + ); } #[test] ++++++ rumdl.obsinfo ++++++ --- /var/tmp/diff_new_pack.RCk5Qs/_old 2026-05-20 15:26:54.788052834 +0200 +++ /var/tmp/diff_new_pack.RCk5Qs/_new 2026-05-20 15:26:54.808053658 +0200 @@ -1,5 +1,5 @@ name: rumdl -version: 0.1.94 -mtime: 1779099102 -commit: fbb90ac101470d2c249e3bc5d9a8915de477f3e4 +version: 0.1.95 +mtime: 1779221704 +commit: b2164bb1b33cbc3b416f686180033b8be7374f37 ++++++ vendor.tar.zst ++++++ /work/SRC/openSUSE:Factory/rumdl/vendor.tar.zst /work/SRC/openSUSE:Factory/.rumdl.new.1966/vendor.tar.zst differ: char 7, line 1
