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-06-22 17:27:27 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/rumdl (Old) and /work/SRC/openSUSE:Factory/.rumdl.new.1956 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "rumdl" Mon Jun 22 17:27:27 2026 rev:80 rq:1360713 version:0.2.20 Changes: -------- --- /work/SRC/openSUSE:Factory/rumdl/rumdl.changes 2026-06-19 17:22:19.364027888 +0200 +++ /work/SRC/openSUSE:Factory/.rumdl.new.1956/rumdl.changes 2026-06-22 17:28:06.082949238 +0200 @@ -1,0 +2,10 @@ +Sat Jun 20 10:08:16 UTC 2026 - Johannes Kastl <[email protected]> + +- Upate to version 0.2.20: + * Fixed + - cli: redirect removed --list-rules to the canonical commands + (7a3842d) + - parser: correct code-region detection for comments and JSX + fences (17f975c) + +------------------------------------------------------------------- Old: ---- rumdl-0.2.19.obscpio New: ---- rumdl-0.2.20.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ rumdl.spec ++++++ --- /var/tmp/diff_new_pack.8K4rRc/_old 2026-06-22 17:28:09.183057229 +0200 +++ /var/tmp/diff_new_pack.8K4rRc/_new 2026-06-22 17:28:09.187057368 +0200 @@ -17,7 +17,7 @@ Name: rumdl -Version: 0.2.19 +Version: 0.2.20 Release: 0 Summary: Markdown Linter written in Rust License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.8K4rRc/_old 2026-06-22 17:28:09.231058900 +0200 +++ /var/tmp/diff_new_pack.8K4rRc/_new 2026-06-22 17:28:09.231058900 +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.2.19</param> + <param name="revision">v0.2.20</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.8K4rRc/_old 2026-06-22 17:28:09.267060155 +0200 +++ /var/tmp/diff_new_pack.8K4rRc/_new 2026-06-22 17:28:09.271060295 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/rvben/rumdl.git</param> - <param name="changesrevision">b7d94ba84336eca6c071d9e357de1dcacb6e004c</param></service></servicedata> + <param name="changesrevision">e8147c3e3ebbcc4a7cddfc9869afb7ff0dd4402b</param></service></servicedata> (No newline at EOF) ++++++ rumdl-0.2.19.obscpio -> rumdl-0.2.20.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/CHANGELOG.md new/rumdl-0.2.20/CHANGELOG.md --- old/rumdl-0.2.19/CHANGELOG.md 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/CHANGELOG.md 2026-06-19 18:46:00.000000000 +0200 @@ -59,6 +59,14 @@ + +## [0.2.20](https://github.com/rvben/rumdl/compare/v0.2.19...v0.2.20) - 2026-06-19 + +### Fixed + +- **cli**: redirect removed --list-rules to the canonical commands ([7a3842d](https://github.com/rvben/rumdl/commit/7a3842d2f58d305e5bbd81e6f2cea3b996c4f410)) +- **parser**: correct code-region detection for comments and JSX fences ([17f975c](https://github.com/rvben/rumdl/commit/17f975ced497177f798ea4d23ba209a7e8bcd223)) + ## [0.2.19](https://github.com/rvben/rumdl/compare/v0.2.18...v0.2.19) - 2026-06-18 ### Added diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/Cargo.lock new/rumdl-0.2.20/Cargo.lock --- old/rumdl-0.2.19/Cargo.lock 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/Cargo.lock 2026-06-19 18:46:00.000000000 +0200 @@ -2283,7 +2283,7 @@ [[package]] name = "rumdl" -version = "0.2.19" +version = "0.2.20" dependencies = [ "anyhow", "assert_cmd", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/Cargo.toml new/rumdl-0.2.20/Cargo.toml --- old/rumdl-0.2.19/Cargo.toml 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/Cargo.toml 2026-06-19 18:46:00.000000000 +0200 @@ -1,6 +1,6 @@ [package] name = "rumdl" -version = "0.2.19" +version = "0.2.20" 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.2.19/README.md new/rumdl-0.2.20/README.md --- old/rumdl-0.2.19/README.md 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/README.md 2026-06-19 18:46:00.000000000 +0200 @@ -207,7 +207,7 @@ mise install rumdl # Use a specific version for the project -mise use [email protected] +mise use [email protected] ``` ### Using Nix (macOS/Linux) @@ -436,7 +436,7 @@ ```yaml repos: - repo: https://github.com/rvben/rumdl-pre-commit - rev: v0.2.19 + rev: v0.2.20 hooks: - id: rumdl # Lint only; add args [--fix] to auto-fix - id: rumdl-fmt # Pure format, always exits 0 @@ -452,7 +452,7 @@ ```yaml repos: - repo: https://github.com/rvben/rumdl-pre-commit - rev: v0.2.19 + rev: v0.2.20 hooks: - id: rumdl args: [--fix] # Auto-fix violations in place @@ -469,7 +469,7 @@ ```yaml repos: - repo: https://github.com/rvben/rumdl-pre-commit - rev: v0.2.19 + rev: v0.2.20 hooks: - id: rumdl args: [--no-exclude] # Disable all exclude patterns @@ -606,7 +606,6 @@ - `-f, --fix`: Automatically fix issues where possible - `--diff`: Show diff of what would be fixed instead of fixing files - `-w, --watch`: Run in watch mode by re-running whenever files change -- `-l, --list-rules`: List all available rules - `-d, --disable <rules>`: Disable specific rules (comma-separated) - `-e, --enable <rules>`: Enable only specific rules (comma-separated) - `--exclude <patterns>`: Exclude specific files or directories (comma-separated glob patterns) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/docs/global-settings.md new/rumdl-0.2.20/docs/global-settings.md --- old/rumdl-0.2.19/docs/global-settings.md 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/docs/global-settings.md 2026-06-19 18:46:00.000000000 +0200 @@ -1374,7 +1374,7 @@ ```yaml - repo: https://github.com/rvben/rumdl-pre-commit - rev: v0.2.19 + rev: v0.2.20 hooks: - id: rumdl args: [--config=.rumdl.toml] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/npm/cli-darwin-arm64/package.json new/rumdl-0.2.20/npm/cli-darwin-arm64/package.json --- old/rumdl-0.2.19/npm/cli-darwin-arm64/package.json 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/npm/cli-darwin-arm64/package.json 2026-06-19 18:46:00.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-darwin-arm64", - "version": "0.2.19", + "version": "0.2.20", "description": "rumdl binary for macOS ARM64 (Apple Silicon)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/npm/cli-darwin-x64/package.json new/rumdl-0.2.20/npm/cli-darwin-x64/package.json --- old/rumdl-0.2.19/npm/cli-darwin-x64/package.json 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/npm/cli-darwin-x64/package.json 2026-06-19 18:46:00.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-darwin-x64", - "version": "0.2.19", + "version": "0.2.20", "description": "rumdl binary for macOS x64 (Intel)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/npm/cli-linux-arm64/package.json new/rumdl-0.2.20/npm/cli-linux-arm64/package.json --- old/rumdl-0.2.19/npm/cli-linux-arm64/package.json 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/npm/cli-linux-arm64/package.json 2026-06-19 18:46:00.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-linux-arm64", - "version": "0.2.19", + "version": "0.2.20", "description": "rumdl binary for Linux ARM64 (glibc)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/npm/cli-linux-arm64-musl/package.json new/rumdl-0.2.20/npm/cli-linux-arm64-musl/package.json --- old/rumdl-0.2.19/npm/cli-linux-arm64-musl/package.json 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/npm/cli-linux-arm64-musl/package.json 2026-06-19 18:46:00.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-linux-arm64-musl", - "version": "0.2.19", + "version": "0.2.20", "description": "rumdl binary for Linux ARM64 (musl/Alpine)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/npm/cli-linux-x64/package.json new/rumdl-0.2.20/npm/cli-linux-x64/package.json --- old/rumdl-0.2.19/npm/cli-linux-x64/package.json 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/npm/cli-linux-x64/package.json 2026-06-19 18:46:00.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-linux-x64", - "version": "0.2.19", + "version": "0.2.20", "description": "rumdl binary for Linux x64 (glibc)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/npm/cli-linux-x64-musl/package.json new/rumdl-0.2.20/npm/cli-linux-x64-musl/package.json --- old/rumdl-0.2.19/npm/cli-linux-x64-musl/package.json 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/npm/cli-linux-x64-musl/package.json 2026-06-19 18:46:00.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-linux-x64-musl", - "version": "0.2.19", + "version": "0.2.20", "description": "rumdl binary for Linux x64 (musl/Alpine)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/npm/cli-win32-x64/package.json new/rumdl-0.2.20/npm/cli-win32-x64/package.json --- old/rumdl-0.2.19/npm/cli-win32-x64/package.json 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/npm/cli-win32-x64/package.json 2026-06-19 18:46:00.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-win32-x64", - "version": "0.2.19", + "version": "0.2.20", "description": "rumdl binary for Windows x64", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/npm/rumdl/package.json new/rumdl-0.2.20/npm/rumdl/package.json --- old/rumdl-0.2.19/npm/rumdl/package.json 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/npm/rumdl/package.json 2026-06-19 18:46:00.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "rumdl", - "version": "0.2.19", + "version": "0.2.20", "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.2.19", - "@rumdl/cli-darwin-arm64": "0.2.19", - "@rumdl/cli-linux-x64": "0.2.19", - "@rumdl/cli-linux-arm64": "0.2.19", - "@rumdl/cli-linux-x64-musl": "0.2.19", - "@rumdl/cli-linux-arm64-musl": "0.2.19", - "@rumdl/cli-win32-x64": "0.2.19" + "@rumdl/cli-darwin-x64": "0.2.20", + "@rumdl/cli-darwin-arm64": "0.2.20", + "@rumdl/cli-linux-x64": "0.2.20", + "@rumdl/cli-linux-arm64": "0.2.20", + "@rumdl/cli-linux-x64-musl": "0.2.20", + "@rumdl/cli-linux-arm64-musl": "0.2.20", + "@rumdl/cli-win32-x64": "0.2.20" } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/src/cli_types.rs new/rumdl-0.2.20/src/cli_types.rs --- old/rumdl-0.2.19/src/cli_types.rs 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/src/cli_types.rs 2026-06-19 18:46:00.000000000 +0200 @@ -134,8 +134,9 @@ )] pub check: bool, - /// List all available rules - #[arg(short, long, default_value = "false")] + /// Deprecated and hidden: rule listing moved to `rumdl rule`. When passed, + /// `run_check` prints guidance to the canonical commands and exits. + #[arg(short = 'l', long, hide = true, default_value = "false")] pub list_rules: bool, #[command(flatten)] @@ -224,8 +225,9 @@ #[arg(long, help = "Exit with code 1 if any formatting changes would be made (for CI)")] pub check: bool, - /// Hidden compatibility flag from check - #[arg(short, long, hide = true, default_value = "false")] + /// Deprecated and hidden compatibility flag from check; routed to the same + /// guidance path as `check --list-rules`. + #[arg(short = 'l', long, hide = true, default_value = "false")] pub list_rules: bool, #[command(flatten)] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/src/commands/check.rs new/rumdl-0.2.20/src/commands/check.rs --- old/rumdl-0.2.19/src/commands/check.rs 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/src/commands/check.rs 2026-06-19 18:46:00.000000000 +0200 @@ -13,6 +13,19 @@ let quiet = args.quiet; let silent = args.silent; + // `--list-rules` / `-l` was removed: rule listing lives in dedicated commands. + // Rather than a bare "unexpected argument" error, point users (especially those + // migrating from other linters who reach for `-l`) at the right commands, and + // exit non-zero so a script relying on it fails loudly instead of silently + // skipping the lint. + if args.list_rules { + eprintln!("{}: `--list-rules` has been removed", "Error".red().bold()); + eprintln!(" To list all rules: rumdl rule"); + eprintln!(" Rules active for your config: rumdl check --verbose"); + eprintln!(" Inspect your effective config: rumdl config"); + exit::tool_error(); + } + // Validate mutually exclusive options if args.diff && args.fix { eprintln!("{}: --diff and --fix cannot be used together", "Error".red().bold()); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/src/lint_context/flavor_detection.rs new/rumdl-0.2.20/src/lint_context/flavor_detection.rs --- old/rumdl-0.2.19/src/lint_context/flavor_detection.rs 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/src/lint_context/flavor_detection.rs 2026-06-19 18:46:00.000000000 +0200 @@ -190,16 +190,17 @@ } } - // Clear false in_code_block for indented content inside JSX blocks. - // Preserve real fenced code blocks by tracking fence markers. + // Reconcile `in_code_block` for content inside JSX blocks. pulldown-cmark + // classifies the whole component as one HTML block, so it neither marks a + // nested fenced code block as code (a false negative that let MD034 rewrite + // URLs inside ```bash``` fences - issue #678) nor is reliable about 4-space + // indented content (a false positive). Re-derive the flag from the fence + // markers: lines inside a fence are code, everything else is not. let mut fenced_code = FencedCodeTracker::new(); for line in lines.iter_mut() { if line.in_jsx_block { let trimmed = line.content(content).trim(); - let in_fenced = fenced_code.process_line(trimmed); - if !in_fenced { - line.in_code_block = false; - } + line.in_code_block = fenced_code.process_line(trimmed); } else { fenced_code.reset(); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/src/lint_context/mod.rs new/rumdl-0.2.20/src/lint_context/mod.rs --- old/rumdl-0.2.19/src/lint_context/mod.rs 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/src/lint_context/mod.rs 2026-06-19 18:46:00.000000000 +0200 @@ -152,11 +152,26 @@ let line_to_list = parse_result.line_to_list; let list_start_values = parse_result.list_start_values; - // Pre-compute HTML comment ranges ONCE for all operations + // Pre-compute HTML comment ranges ONCE for all operations. + // Code-span and fenced-code-block ranges are passed so `<!--`/`-->` + // inside code are treated as literal text, not comment delimiters that + // could pair across code regions on different lines. Only *fenced* blocks + // are used: pulldown-cmark misclassifies 4-space-indented content inside + // containers (MkDocs admonitions, etc.) as indented code blocks, and a + // real comment indented there must still be recognized as a comment. + let fenced_code_block_ranges: Vec<(usize, usize)> = code_block_details + .iter() + .filter(|detail| detail.is_fenced) + .map(|detail| (detail.start, detail.end)) + .collect(); let html_comment_ranges = profile_section!( "HTML comment ranges", profile, - crate::utils::skip_context::compute_html_comment_ranges(content) + crate::utils::skip_context::compute_html_comment_ranges_filtered( + content, + &code_span_ranges, + &fenced_code_block_ranges + ) ); // Pre-compute autodoc block ranges (avoids O(n^2) scaling) @@ -367,6 +382,33 @@ } } code_blocks = new_code_blocks; + + // Add byte ranges for fenced code blocks nested inside a JSX component. + // pulldown-cmark classifies the whole component as one HTML block and + // emits no code-block range for the fence, so the split loop above + // (which can only narrow existing ranges) never adds it. Derive the + // ranges from the per-line in_code_block flags detect_jsx_blocks set, + // so byte-range consumers (e.g. MD011, MD044) skip the fence content. + let mut jsx_fence_ranges: Vec<(usize, usize)> = Vec::new(); + let mut run: Option<(usize, usize)> = None; + for line in &lines { + if line.in_jsx_block && line.in_code_block { + let line_end = line.byte_offset + line.byte_len; + match &mut run { + Some((_, end)) => *end = line_end, + None => run = Some((line.byte_offset, line_end)), + } + } else if let Some(r) = run.take() { + jsx_fence_ranges.push(r); + } + } + if let Some(r) = run.take() { + jsx_fence_ranges.push(r); + } + if !jsx_fence_ranges.is_empty() { + code_blocks.extend(jsx_fence_ranges); + code_blocks.sort_by_key(|&(start, _)| start); + } } // Detect Azure DevOps colon code fences and extend code_blocks so that diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/src/rules/md011_no_reversed_links.rs new/rumdl-0.2.20/src/rules/md011_no_reversed_links.rs --- old/rumdl-0.2.19/src/rules/md011_no_reversed_links.rs 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/src/rules/md011_no_reversed_links.rs 2026-06-19 18:46:00.000000000 +0200 @@ -405,6 +405,24 @@ } #[test] + fn test_md011_reversed_link_in_jsx_nested_fence_not_flagged() { + // A reversed link inside a fenced code block nested in a JSX component is + // code, not prose. pulldown-cmark classifies the component as one HTML + // block and emits no code-block range for the fence, so MD011's + // byte-range code check would flag it (and `fmt` would corrupt the code) + // unless the JSX fence range is added to ctx.code_blocks. + let rule = MD011NoReversedLinks; + let content = + "<Steps>\n <Step>\n```text\nsee (this)[https://example.com] reversed\n```\n </Step>\n</Steps>\n"; + let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MDX, None); + let warnings = rule.check(&ctx).unwrap(); + assert!( + warnings.is_empty(), + "reversed link inside a JSX-nested fence must not be flagged: {warnings:?}" + ); + } + + #[test] fn test_md011_dataview_bracket_syntax_obsidian() { let rule = MD011NoReversedLinks; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/src/rules/md032_blanks_around_lists.rs new/rumdl-0.2.20/src/rules/md032_blanks_around_lists.rs --- old/rumdl-0.2.19/src/rules/md032_blanks_around_lists.rs 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/src/rules/md032_blanks_around_lists.rs 2026-06-19 18:46:00.000000000 +0200 @@ -2956,6 +2956,36 @@ } #[test] + fn test_code_span_html_comment_delimiters_no_false_positive() { + // Issue #679: `<!--` and `-->` inside inline code spans on different lines + // must not be parsed as a single multi-line HTML comment. When they were, + // the blank lines between them were treated as "inside a comment" + // (transparent), so MD032 saw the list as lacking surrounding blanks and + // emitted false "list should be followed/preceded by blank line" warnings. + let content = "Text before list.\n\n1. A list item with `<!--` in a code span\n\n### Heading After\n\n1. Another item with `-->` in it\n"; + let warnings = lint(content); + assert_eq!( + warnings.len(), + 0, + "code-span HTML comment delimiters must not cause MD032 false positives, got: {warnings:?}" + ); + } + + #[test] + fn test_code_span_html_comment_delimiters_fix_is_idempotent() { + // Issue #679: the false positives above drove a non-converging `--fix` + // loop - each pass inserted a blank line, shifting bytes so the spurious + // comment range re-matched and the rule "found" the missing blank again. + // The correct fix is a no-op because the content is already well-formed. + let content = "Text before list.\n\n1. A list item with `<!--` in a code span\n\n### Heading After\n\n1. Another item with `-->` in it\n"; + let fixed = fix(content); + assert_eq!( + fixed, content, + "MD032 fix must be a no-op for content whose only `<!--`/`-->` are inside code spans" + ); + } + + #[test] fn test_blockquote_list_at_document_end() { // List at end of document (no trailing content) let content = "> Some text\n>\n> - item 1\n> - item 2"; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/src/rules/md034_no_bare_urls.rs new/rumdl-0.2.20/src/rules/md034_no_bare_urls.rs --- old/rumdl-0.2.19/src/rules/md034_no_bare_urls.rs 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/src/rules/md034_no_bare_urls.rs 2026-06-19 18:46:00.000000000 +0200 @@ -682,6 +682,83 @@ ); } + /// Issue #678: a URL inside a fenced code block that is nested within a JSX/MDX + /// component (e.g. `<Steps><Step>`) is code, not bare prose. It must not be + /// flagged, and `fix` must not rewrite it (which would corrupt the command). + #[test] + fn test_url_in_fenced_code_block_inside_jsx_not_flagged() { + let rule = MD034NoBareUrls; + let content = "# Title\n\n<Steps>\n <Step title=\"Send a request\">\n```bash\ncurl https://example.com/api\n```\n </Step>\n</Steps>\n"; + let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::MDX, None); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty(), + "URL in a fenced code block nested in a JSX component must not be flagged: {result:?}" + ); + } + + /// The same code block must be left byte-for-byte intact by `fix` (no + /// `<https://...>` rewrite that breaks a copy-pasteable command). + #[test] + fn test_fix_does_not_rewrite_url_in_fenced_code_block_inside_jsx() { + let rule = MD034NoBareUrls; + let content = "# Title\n\n<Steps>\n <Step title=\"Send a request\">\n```bash\ncurl https://example.com/api\n```\n </Step>\n</Steps>\n"; + let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::MDX, None); + let fixed = rule.fix(&ctx).unwrap(); + assert_eq!( + fixed, content, + "fix must not rewrite a URL inside a JSX-nested fenced code block" + ); + } + + /// Control: a bare URL in the JSX *body* (outside any fence) is genuine prose + /// and must still be flagged, so the fence exemption is not over-broad. + #[test] + fn test_bare_url_in_jsx_body_outside_fence_still_flagged() { + let rule = MD034NoBareUrls; + let content = "# Title\n\n<Steps>\n <Step title=\"Send a request\">\n Visit https://example.com/api now.\n </Step>\n</Steps>\n"; + let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::MDX, None); + let result = rule.check(&ctx).unwrap(); + assert_eq!( + result.len(), + 1, + "A bare URL in the JSX body (not in a fence) must still be flagged: {result:?}" + ); + } + + /// A `<!--` inside a fenced code block is literal, not a comment opener, so it + /// must not pair with a later `-->` to form a comment range that masks a real + /// bare URL between them (the code-block counterpart to the code-span fix). + #[test] + fn test_bare_url_not_masked_by_comment_delimiter_in_code_block() { + let rule = MD034NoBareUrls; + let content = + "# T\n\n```text\n<!-- literal opener, not a comment\n```\n\nhttps://example.com should be flagged\n\n-->\n"; + let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert_eq!(result.len(), 1, "the bare URL must still be flagged: {result:?}"); + assert!( + result[0].message.contains("example.com"), + "the flagged URL must be the bare one: {result:?}" + ); + } + + /// Only *fenced* code blocks suppress `<!--`/`-->` as literal. A real HTML + /// comment indented inside a MkDocs admonition (which pulldown-cmark + /// misclassifies as an indented code block) must still be recognized as a + /// comment, so its bare URL stays skipped. + #[test] + fn test_bare_url_in_indented_comment_in_admonition_still_skipped() { + let rule = MD034NoBareUrls; + let content = "# T\n\n!!! note\n Some text.\n\n <!--\n https://example.com\n -->\n"; + let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty(), + "URL inside an indented HTML comment in an admonition must not be flagged: {result:?}" + ); + } + /// Issue #649: a URL that is a JSX component attribute value (e.g. `href="..."`) /// is a string prop, not bare prose. Wrapping it in angle brackets produces /// invalid JSX, so MD034 must not flag it under the MDX flavor. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/src/utils/regex_cache.rs new/rumdl-0.2.20/src/utils/regex_cache.rs --- old/rumdl-0.2.19/src/utils/regex_cache.rs 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/src/utils/regex_cache.rs 2026-06-19 18:46:00.000000000 +0200 @@ -427,9 +427,6 @@ // Handles multi-line content with embedded quotes and newlines pub static HUGO_SHORTCODE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\{\{[<%][\s\S]*?[%>]\}\}").unwrap()); -// HTML comment pattern -pub static HTML_COMMENT_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"<!--[\s\S]*?-->").unwrap()); - // HTML heading pattern (matches <h1> through <h6> tags) // Uses FancyRegex because the pattern requires a backreference (\1) pub static HTML_HEADING_PATTERN: LazyLock<FancyRegex> = diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/src/utils/skip_context.rs new/rumdl-0.2.20/src/utils/skip_context.rs --- old/rumdl-0.2.19/src/utils/skip_context.rs 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/src/utils/skip_context.rs 2026-06-19 18:46:00.000000000 +0200 @@ -12,7 +12,6 @@ use crate::utils::mkdocs_icons; use crate::utils::mkdocs_snippets; use crate::utils::mkdocs_tabs; -use crate::utils::regex_cache::HTML_COMMENT_PATTERN; use regex::Regex; use std::sync::LazyLock; @@ -33,16 +32,74 @@ pub end: usize, } -/// Pre-compute all HTML comment ranges in the content -/// Returns a sorted vector of byte ranges for efficient lookup +/// Pre-compute all HTML comment ranges in the content. +/// Returns a sorted vector of byte ranges for efficient lookup. pub fn compute_html_comment_ranges(content: &str) -> Vec<ByteRange> { - HTML_COMMENT_PATTERN - .find_iter(content) - .map(|m| ByteRange { - start: m.start(), - end: m.end(), - }) - .collect() + compute_html_comment_ranges_filtered(content, &[], &[]) +} + +/// Pre-compute HTML comment ranges, treating `<!--`/`-->` inside inline code +/// spans or fenced/indented code blocks as literal text rather than comment +/// delimiters. +/// +/// Inside code (a backtick code span or a code block), `<!--` and `-->` are +/// literal. A naive `<!--[\s\S]*?-->` scan would pair a `<!--` in one code +/// region with a `-->` in a later region, spuriously marking every line between +/// them as "inside an HTML comment". That made content invisible to rules that +/// skip comment lines: MD032's blank-line check produced false positives and a +/// non-converging `--fix` loop, and MD034 silently dropped a real bare URL that +/// fell between a code-block `<!--` and a later `-->`. +/// +/// To stay correct even when a literal delimiter precedes a genuine comment +/// (`` `<!--` <!-- real --> ``), this scans for the next `<!--` opener that is +/// not in code, then the next `-->` closer that is not in code - it does not +/// filter completed regex matches. `code_span_ranges` and `code_block_ranges` +/// are the parser's half-open `[start, end)` byte ranges (`ParseResult`). With +/// no code ranges this is equivalent to the lazy regex: an opener pairs with the +/// first following closer, and an opener with no closer yields no range. +pub fn compute_html_comment_ranges_filtered( + content: &str, + code_span_ranges: &[(usize, usize)], + code_block_ranges: &[(usize, usize)], +) -> Vec<ByteRange> { + let in_code = |pos: usize| { + code_span_ranges.iter().any(|&(start, end)| pos >= start && pos < end) + || code_block_ranges.iter().any(|&(start, end)| pos >= start && pos < end) + }; + + let mut ranges = Vec::new(); + let mut search_from = 0; + while let Some(rel) = content[search_from..].find("<!--") { + let open = search_from + rel; + if in_code(open) { + // Literal `<!--` inside code: not a comment opener. + search_from = open + "<!--".len(); + continue; + } + // Find the next `-->` that is not itself inside code. + let mut close_from = open + "<!--".len(); + let end = loop { + let Some(crel) = content[close_from..].find("-->") else { + break None; + }; + let close = close_from + crel; + if in_code(close) { + close_from = close + "-->".len(); + continue; + } + break Some(close + "-->".len()); + }; + match end { + Some(end) => { + ranges.push(ByteRange { start: open, end }); + search_from = end; + } + // Unterminated comment (no closer anywhere): the regex would not + // match either, so emit nothing and stop. + None => break, + } + } + ranges } /// Check if a byte position is within any of the pre-computed HTML comment ranges @@ -111,16 +168,6 @@ flavor == MarkdownFlavor::MkDocs && mkdocs_critic::contains_critic_markup(line) } -/// Check if a byte position is within an HTML comment -pub fn is_in_html_comment(content: &str, byte_pos: usize) -> bool { - for m in HTML_COMMENT_PATTERN.find_iter(content) { - if m.start() <= byte_pos && byte_pos < m.end() { - return true; - } - } - false -} - /// Check if a byte position is within an HTML tag pub fn is_in_html_tag(ctx: &LintContext, byte_pos: usize) -> bool { for html_tag in ctx.html_tags().iter() { @@ -530,9 +577,104 @@ #[test] fn test_html_comment_detection() { let content = "Text <!-- comment --> more text"; - assert!(is_in_html_comment(content, 10)); // Inside comment - assert!(!is_in_html_comment(content, 0)); // Before comment - assert!(!is_in_html_comment(content, 25)); // After comment + let ranges = compute_html_comment_ranges(content); + assert!(is_in_html_comment_ranges(&ranges, 10)); // Inside comment + assert!(!is_in_html_comment_ranges(&ranges, 0)); // Before comment + assert!(!is_in_html_comment_ranges(&ranges, 25)); // After comment + } + + #[test] + fn test_compute_html_comment_ranges_ignores_code_span_delimiters() { + // `<!--` and `-->` inside inline code spans on different lines must not + // pair into a multi-line HTML comment (issue #679). + let content = "a `<!--` b\n\nc `-->` d"; + let open = content.find("<!--").unwrap(); + let close = content.find("-->").unwrap(); + // Code spans covering the two backtick-delimited tokens. + let code_spans = [ + (content.find('`').unwrap(), open + "<!--".len() + 1), + (content.rfind("` d").unwrap() - "-->".len(), close + "-->".len() + 1), + ]; + + // Without code-span awareness the pattern spans open..close (the bug). + assert!( + !compute_html_comment_ranges(content).is_empty(), + "sanity: raw pattern matches across the code spans" + ); + // With code-span awareness the spurious match is dropped. + assert!( + compute_html_comment_ranges_filtered(content, &code_spans, &[]).is_empty(), + "a `<!--`/`-->` pair inside code spans must not be treated as a comment" + ); + } + + #[test] + fn test_compute_html_comment_ranges_ignores_code_block_delimiters() { + // A `<!--` inside a code block must not pair with a later `-->` outside it + // (the code-block counterpart of the code-span case). + let content = "```\n<!-- literal\n```\n\nhttps://example.com\n\n-->\n"; + let block_end = content.find("```\n\n").unwrap() + "```".len(); + let code_blocks = [(0usize, block_end)]; + assert!( + compute_html_comment_ranges_filtered(content, &[], &code_blocks).is_empty(), + "a `<!--` inside a code block must not open a comment that spans to a later `-->`" + ); + // A real comment whose opener is outside the block is still detected. + let real = "```\n<!-- literal\n```\n\n<!-- real --> tail"; + let real_block_end = real.find("```\n\n").unwrap() + "```".len(); + let ranges = compute_html_comment_ranges_filtered(real, &[], &[(0usize, real_block_end)]); + assert_eq!(ranges.len(), 1); + assert_eq!(ranges[0].start, real.find("<!-- real").unwrap()); + } + + #[test] + fn test_compute_html_comment_ranges_keeps_real_comments() { + // A genuine comment whose `<!--` is not inside a code span is still + // detected, even when an unrelated code span exists elsewhere. + let content = "text `code` <!-- real comment --> more"; + let code_spans = [(content.find('`').unwrap(), content.find("` ").unwrap() + 1)]; + let ranges = compute_html_comment_ranges_filtered(content, &code_spans, &[]); + assert_eq!(ranges.len(), 1, "the real comment must still be detected"); + let comment_start = content.find("<!--").unwrap(); + assert_eq!(ranges[0].start, comment_start); + } + + #[test] + fn test_compute_html_comment_ranges_real_comment_after_code_span_opener() { + // A code span containing `<!--` must not consume a real comment that + // follows it: skipping the literal opener, the scan must still discover + // the genuine `<!-- ... -->` and mark its content as a comment. + let content = "a `<!--` then <!-- real --> end"; + let code_spans = [(content.find('`').unwrap(), content.find("` then").unwrap() + 1)]; + let ranges = compute_html_comment_ranges_filtered(content, &code_spans, &[]); + assert_eq!( + ranges.len(), + 1, + "the real comment after a code-span opener must be detected" + ); + let real_open = content.find("<!-- real").unwrap(); + assert_eq!( + ranges[0].start, real_open, + "range must start at the real comment, not the code-span opener" + ); + assert_eq!(ranges[0].end, content.find("--> end").unwrap() + "-->".len()); + } + + #[test] + fn test_compute_html_comment_ranges_closer_inside_code_span_is_not_a_closer() { + // A real comment's closing `-->` that lands inside a code span is literal; + // the scan must continue to the next real `-->`. + let content = "<!-- open `-->` still open --> done"; + let first_close = content.find("`-->`").unwrap() + 1; + let code_spans = [(content.find('`').unwrap(), content.find("` still").unwrap() + 1)]; + let ranges = compute_html_comment_ranges_filtered(content, &code_spans, &[]); + assert_eq!(ranges.len(), 1); + assert_eq!(ranges[0].start, 0); + let real_close_end = content.find("--> done").unwrap() + "-->".len(); + assert_eq!( + ranges[0].end, real_close_end, + "must close at the real --> ({real_close_end}), not the one in the code span ({first_close})" + ); } #[test] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/tests/cli/cli_list_rules_removed_test.rs new/rumdl-0.2.20/tests/cli/cli_list_rules_removed_test.rs --- old/rumdl-0.2.19/tests/cli/cli_list_rules_removed_test.rs 1970-01-01 01:00:00.000000000 +0100 +++ new/rumdl-0.2.20/tests/cli/cli_list_rules_removed_test.rs 2026-06-19 18:46:00.000000000 +0200 @@ -0,0 +1,111 @@ +//! Issue #680: `rumdl check --list-rules` was advertised in help/README but the +//! flag was inert (parsed, never read), so it silently ran a normal check. +//! +//! The flag is now hidden and deprecated: `check`/`fmt` no longer advertise it, +//! and using it fails loudly (exit 2) with guidance pointing at the canonical +//! commands (`rumdl rule`, `rumdl check --verbose`, `rumdl config`) instead of a +//! bare "unexpected argument" error. + +use std::process::Command; +use tempfile::tempdir; + +#[test] +fn test_check_list_rules_redirects_with_guidance() { + let temp_dir = tempdir().unwrap(); + let rumdl_exe = env!("CARGO_BIN_EXE_rumdl"); + + let output = Command::new(rumdl_exe) + .current_dir(temp_dir.path()) + .args(["check", "--list-rules"]) + .output() + .expect("Failed to execute command"); + + // Fails loudly (exit 2) so a script using it does not silently skip linting. + assert_eq!( + output.status.code(), + Some(2), + "`check --list-rules` must fail as a usage error, not run a lint" + ); + // ...and points at the canonical rule-listing command rather than a bare error. + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("rumdl rule"), + "the message should redirect to `rumdl rule`, got stderr: {stderr}" + ); +} + +#[test] +fn test_check_list_rules_short_flag_redirects() { + let temp_dir = tempdir().unwrap(); + let rumdl_exe = env!("CARGO_BIN_EXE_rumdl"); + + let output = Command::new(rumdl_exe) + .current_dir(temp_dir.path()) + .args(["check", "-l"]) + .output() + .expect("Failed to execute command"); + + assert_eq!(output.status.code(), Some(2), "`check -l` must fail as a usage error"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("rumdl rule"), + "`-l` should redirect to `rumdl rule`: {stderr}" + ); +} + +#[test] +fn test_fmt_list_rules_redirects() { + let temp_dir = tempdir().unwrap(); + let rumdl_exe = env!("CARGO_BIN_EXE_rumdl"); + + let output = Command::new(rumdl_exe) + .current_dir(temp_dir.path()) + .args(["fmt", "--list-rules"]) + .output() + .expect("Failed to execute command"); + + assert_eq!( + output.status.code(), + Some(2), + "`fmt --list-rules` must fail as a usage error" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("rumdl rule"), + "`fmt --list-rules` should redirect: {stderr}" + ); +} + +#[test] +fn test_list_rules_flag_is_hidden_from_help() { + // The flag must not be advertised in help anymore (that false advertising was + // the original bug); it only exists as a hidden redirect. + let rumdl_exe = env!("CARGO_BIN_EXE_rumdl"); + let output = Command::new(rumdl_exe) + .args(["check", "--help"]) + .output() + .expect("Failed to execute command"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + !stdout.contains("--list-rules"), + "`check --help` must not advertise the deprecated --list-rules flag" + ); +} + +#[test] +fn test_rule_subcommand_lists_rules() { + // The canonical replacement: `rumdl rule` lists all available rules. + let rumdl_exe = env!("CARGO_BIN_EXE_rumdl"); + + let output = Command::new(rumdl_exe) + .arg("rule") + .output() + .expect("Failed to execute command"); + + assert!(output.status.success(), "`rumdl rule` should succeed"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("MD001"), + "`rumdl rule` should list rules including MD001, got: {stdout}" + ); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.2.19/tests/cli/mod.rs new/rumdl-0.2.20/tests/cli/mod.rs --- old/rumdl-0.2.19/tests/cli/mod.rs 2026-06-18 22:05:29.000000000 +0200 +++ new/rumdl-0.2.20/tests/cli/mod.rs 2026-06-19 18:46:00.000000000 +0200 @@ -9,6 +9,7 @@ mod cli_flag_precedence_test; mod cli_flavor_test; mod cli_integration_tests; +mod cli_list_rules_removed_test; mod cli_lsp_fix_consistency; mod cli_respect_gitignore_test; mod cli_rules_wrapper_test; ++++++ rumdl.obsinfo ++++++ --- /var/tmp/diff_new_pack.8K4rRc/_old 2026-06-22 17:28:10.555105024 +0200 +++ /var/tmp/diff_new_pack.8K4rRc/_new 2026-06-22 17:28:10.559105163 +0200 @@ -1,5 +1,5 @@ name: rumdl -version: 0.2.19 -mtime: 1781813129 -commit: b7d94ba84336eca6c071d9e357de1dcacb6e004c +version: 0.2.20 +mtime: 1781887560 +commit: e8147c3e3ebbcc4a7cddfc9869afb7ff0dd4402b ++++++ vendor.tar.zst ++++++ /work/SRC/openSUSE:Factory/rumdl/vendor.tar.zst /work/SRC/openSUSE:Factory/.rumdl.new.1956/vendor.tar.zst differ: char 7, line 1
