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-03-19 17:39:39 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/rumdl (Old) and /work/SRC/openSUSE:Factory/.rumdl.new.8177 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "rumdl" Thu Mar 19 17:39:39 2026 rev:46 rq:1341081 version:0.1.54 Changes: -------- --- /work/SRC/openSUSE:Factory/rumdl/rumdl.changes 2026-03-17 19:05:37.867490352 +0100 +++ /work/SRC/openSUSE:Factory/.rumdl.new.8177/rumdl.changes 2026-03-19 17:41:20.643670878 +0100 @@ -1,0 +2,15 @@ +Thu Mar 19 06:13:36 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.1.54: + * Fixed + - MD013: Lines consisting entirely of inline HTML (e.g., badge + links <a href="..."><img .../></a>) are no longer flagged + when strict = false (#535) + - Two-tier detection: lines where all content is inside HTML + tags, and lines that start/end with tags containing URL + attributes (href, src, srcset, poster) + - HTML-only lines are also treated as paragraph boundaries in + reflow mode, preventing them from being merged into + adjacent prose + +------------------------------------------------------------------- Old: ---- rumdl-0.1.53.obscpio New: ---- rumdl-0.1.54.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ rumdl.spec ++++++ --- /var/tmp/diff_new_pack.WPwok3/_old 2026-03-19 17:41:23.247778760 +0100 +++ /var/tmp/diff_new_pack.WPwok3/_new 2026-03-19 17:41:23.263779423 +0100 @@ -17,7 +17,7 @@ Name: rumdl -Version: 0.1.53 +Version: 0.1.54 Release: 0 Summary: Markdown Linter written in Rust License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.WPwok3/_old 2026-03-19 17:41:23.467787874 +0100 +++ /var/tmp/diff_new_pack.WPwok3/_new 2026-03-19 17:41:23.499789200 +0100 @@ -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.53</param> + <param name="revision">v0.1.54</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.WPwok3/_old 2026-03-19 17:41:23.635794835 +0100 +++ /var/tmp/diff_new_pack.WPwok3/_new 2026-03-19 17:41:23.659795829 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/rvben/rumdl.git</param> - <param name="changesrevision">ce2ee387fc15da333da08f395dc578d5eb5e29b7</param></service></servicedata> + <param name="changesrevision">8eeed34c6f8f3bacc43f59a6a34f3c15dd594b20</param></service></servicedata> (No newline at EOF) ++++++ rumdl-0.1.53.obscpio -> rumdl-0.1.54.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.53/CHANGELOG.md new/rumdl-0.1.54/CHANGELOG.md --- old/rumdl-0.1.53/CHANGELOG.md 2026-03-16 16:29:34.000000000 +0100 +++ new/rumdl-0.1.54/CHANGELOG.md 2026-03-18 17:23:42.000000000 +0100 @@ -7,6 +7,14 @@ ## [Unreleased] +## [0.1.54] - 2026-03-18 + +### Fixed + +- **MD013**: Lines consisting entirely of inline HTML (e.g., badge links `<a href="..."><img .../></a>`) are no longer flagged when `strict = false` ([#535](https://github.com/rvben/rumdl/issues/535)) + - Two-tier detection: lines where all content is inside HTML tags, and lines that start/end with tags containing URL attributes (`href`, `src`, `srcset`, `poster`) + - HTML-only lines are also treated as paragraph boundaries in reflow mode, preventing them from being merged into adjacent prose + ## [0.1.53] - 2026-03-16 ### Fixed diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.53/Cargo.lock new/rumdl-0.1.54/Cargo.lock --- old/rumdl-0.1.53/Cargo.lock 2026-03-16 16:29:34.000000000 +0100 +++ new/rumdl-0.1.54/Cargo.lock 2026-03-18 17:23:42.000000000 +0100 @@ -2247,7 +2247,7 @@ [[package]] name = "rumdl" -version = "0.1.53" +version = "0.1.54" dependencies = [ "anyhow", "assert_cmd", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.53/Cargo.toml new/rumdl-0.1.54/Cargo.toml --- old/rumdl-0.1.53/Cargo.toml 2026-03-16 16:29:34.000000000 +0100 +++ new/rumdl-0.1.54/Cargo.toml 2026-03-18 17:23:42.000000000 +0100 @@ -1,6 +1,6 @@ [package] name = "rumdl" -version = "0.1.53" +version = "0.1.54" 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.53/README.md new/rumdl-0.1.54/README.md --- old/rumdl-0.1.53/README.md 2026-03-16 16:29:34.000000000 +0100 +++ new/rumdl-0.1.54/README.md 2026-03-18 17:23:42.000000000 +0100 @@ -196,7 +196,7 @@ mise install rumdl # Use a specific version for the project -mise use [email protected] +mise use [email protected] ``` ### Using Nix (macOS/Linux) @@ -346,7 +346,7 @@ ```yaml repos: - repo: https://github.com/rvben/rumdl-pre-commit - rev: v0.1.53 + rev: v0.1.54 hooks: - id: rumdl # Lint only (fails on issues) - id: rumdl-fmt # Auto-format and fail if issues remain @@ -368,7 +368,7 @@ ```yaml repos: - repo: https://github.com/rvben/rumdl-pre-commit - rev: v0.1.53 + rev: v0.1.54 hooks: - id: rumdl args: [--no-exclude] # Disable all exclude patterns diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.53/npm/cli-darwin-arm64/package.json new/rumdl-0.1.54/npm/cli-darwin-arm64/package.json --- old/rumdl-0.1.53/npm/cli-darwin-arm64/package.json 2026-03-16 16:29:34.000000000 +0100 +++ new/rumdl-0.1.54/npm/cli-darwin-arm64/package.json 2026-03-18 17:23:42.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-darwin-arm64", - "version": "0.1.53", + "version": "0.1.54", "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.53/npm/cli-darwin-x64/package.json new/rumdl-0.1.54/npm/cli-darwin-x64/package.json --- old/rumdl-0.1.53/npm/cli-darwin-x64/package.json 2026-03-16 16:29:34.000000000 +0100 +++ new/rumdl-0.1.54/npm/cli-darwin-x64/package.json 2026-03-18 17:23:42.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-darwin-x64", - "version": "0.1.53", + "version": "0.1.54", "description": "rumdl binary for macOS x64 (Intel)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.53/npm/cli-linux-arm64/package.json new/rumdl-0.1.54/npm/cli-linux-arm64/package.json --- old/rumdl-0.1.53/npm/cli-linux-arm64/package.json 2026-03-16 16:29:34.000000000 +0100 +++ new/rumdl-0.1.54/npm/cli-linux-arm64/package.json 2026-03-18 17:23:42.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-linux-arm64", - "version": "0.1.53", + "version": "0.1.54", "description": "rumdl binary for Linux ARM64 (glibc)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.53/npm/cli-linux-arm64-musl/package.json new/rumdl-0.1.54/npm/cli-linux-arm64-musl/package.json --- old/rumdl-0.1.53/npm/cli-linux-arm64-musl/package.json 2026-03-16 16:29:34.000000000 +0100 +++ new/rumdl-0.1.54/npm/cli-linux-arm64-musl/package.json 2026-03-18 17:23:42.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-linux-arm64-musl", - "version": "0.1.53", + "version": "0.1.54", "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.53/npm/cli-linux-x64/package.json new/rumdl-0.1.54/npm/cli-linux-x64/package.json --- old/rumdl-0.1.53/npm/cli-linux-x64/package.json 2026-03-16 16:29:34.000000000 +0100 +++ new/rumdl-0.1.54/npm/cli-linux-x64/package.json 2026-03-18 17:23:42.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-linux-x64", - "version": "0.1.53", + "version": "0.1.54", "description": "rumdl binary for Linux x64 (glibc)", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.53/npm/cli-linux-x64-musl/package.json new/rumdl-0.1.54/npm/cli-linux-x64-musl/package.json --- old/rumdl-0.1.53/npm/cli-linux-x64-musl/package.json 2026-03-16 16:29:34.000000000 +0100 +++ new/rumdl-0.1.54/npm/cli-linux-x64-musl/package.json 2026-03-18 17:23:42.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-linux-x64-musl", - "version": "0.1.53", + "version": "0.1.54", "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.53/npm/cli-win32-x64/package.json new/rumdl-0.1.54/npm/cli-win32-x64/package.json --- old/rumdl-0.1.53/npm/cli-win32-x64/package.json 2026-03-16 16:29:34.000000000 +0100 +++ new/rumdl-0.1.54/npm/cli-win32-x64/package.json 2026-03-18 17:23:42.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "@rumdl/cli-win32-x64", - "version": "0.1.53", + "version": "0.1.54", "description": "rumdl binary for Windows x64", "license": "MIT", "repository": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.53/npm/rumdl/package.json new/rumdl-0.1.54/npm/rumdl/package.json --- old/rumdl-0.1.53/npm/rumdl/package.json 2026-03-16 16:29:34.000000000 +0100 +++ new/rumdl-0.1.54/npm/rumdl/package.json 2026-03-18 17:23:42.000000000 +0100 @@ -1,6 +1,6 @@ { "name": "rumdl", - "version": "0.1.53", + "version": "0.1.54", "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.53", - "@rumdl/cli-darwin-arm64": "0.1.53", - "@rumdl/cli-linux-x64": "0.1.53", - "@rumdl/cli-linux-arm64": "0.1.53", - "@rumdl/cli-linux-x64-musl": "0.1.53", - "@rumdl/cli-linux-arm64-musl": "0.1.53", - "@rumdl/cli-win32-x64": "0.1.53" + "@rumdl/cli-darwin-x64": "0.1.54", + "@rumdl/cli-darwin-arm64": "0.1.54", + "@rumdl/cli-linux-x64": "0.1.54", + "@rumdl/cli-linux-arm64": "0.1.54", + "@rumdl/cli-linux-x64-musl": "0.1.54", + "@rumdl/cli-linux-arm64-musl": "0.1.54", + "@rumdl/cli-win32-x64": "0.1.54" } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.53/src/rules/md013_line_length/helpers.rs new/rumdl-0.1.54/src/rules/md013_line_length/helpers.rs --- old/rumdl-0.1.53/src/rules/md013_line_length/helpers.rs 2026-03-16 16:29:34.000000000 +0100 +++ new/rumdl-0.1.54/src/rules/md013_line_length/helpers.rs 2026-03-18 17:23:42.000000000 +0100 @@ -243,6 +243,107 @@ is_link_with_optional_emphasis(s) } +/// Check if a line consists entirely of HTML structure that cannot be +/// meaningfully shortened. Used to exempt HTML-only lines from MD013 in +/// non-strict mode. +/// +/// After stripping blockquote and list markers, a line is exempt if either: +/// +/// 1. All non-whitespace content is inside `<...>` tags (e.g., badges, +/// self-closing images, nested tags with no text between them). +/// 2. The line starts with `<` and ends with `>` AND contains URL-bearing +/// attributes (`href=`, `src=`, `srcset=`, `poster=`). This handles +/// `<a href="url">text</a>` — functionally identical to `[text](url)` +/// which is already exempt as a standalone link. +/// +/// Handles quoted attribute values that may contain `>` characters. +/// +/// Examples that return true: +/// - `<a href="..."><img alt="badge" src="..."/></a>` (all content in tags) +/// - `<img src="..." alt="..." width="..." height="..."/>` (self-closing) +/// - `<a href="...">link text</a>` (HTML link, consistent with markdown link exemption) +/// - `<video src="..." poster="..." controls></video>` (media with URL attrs) +/// +/// Examples that return false: +/// - `Some text <a href="...">link</a>` (text before tags) +/// - `<b>very long bold text</b>` (formatting tag without URL attributes) +/// - `Plain text without any HTML` +pub(crate) fn is_html_only_line(line: &str) -> bool { + let mut s = line.trim_start(); + + // Strip blockquote markers + while let Some(rest) = s.strip_prefix('>') { + s = rest.trim_start(); + } + + // Strip list markers + if is_list_item(s) { + let (_, content) = extract_list_marker_and_content(s); + return is_html_only_content(&content); + } + + is_html_only_content(s) +} + +/// Combined check for HTML-only content. +fn is_html_only_content(s: &str) -> bool { + let s = s.trim(); + if s.is_empty() || !s.starts_with('<') { + return false; + } + + // Check 1: All non-whitespace content is inside HTML tags. + // Covers badges, self-closing images, nested tags with no text between them. + if is_content_all_html_tags(s) { + return true; + } + + // Check 2: Line is entirely wrapped in HTML (starts with <, ends with >) + // and contains URL-bearing attributes. This makes <a href="url">text</a> + // consistent with the existing [text](url) standalone link exemption. + if s.ends_with('>') && (s.contains("href=") || s.contains("src=") || s.contains("srcset=") || s.contains("poster=")) + { + return true; + } + + false +} + +/// Returns true if all non-whitespace content is inside `<...>` delimiters. +fn is_content_all_html_tags(s: &str) -> bool { + let s = s.trim(); + if s.is_empty() || !s.starts_with('<') { + return false; + } + + let mut in_tag = false; + let mut quote_char: Option<char> = None; + let mut found_complete_tag = false; + + for c in s.chars() { + if let Some(q) = quote_char { + if c == q { + quote_char = None; + } + } else if in_tag { + match c { + '"' | '\'' => quote_char = Some(c), + '>' => { + in_tag = false; + found_complete_tag = true; + } + _ => {} + } + } else if c == '<' { + in_tag = true; + } else if !c.is_whitespace() { + return false; + } + } + + found_complete_tag +} + /// Check if content (after stripping list/blockquote markers) is a standalone link, /// optionally wrapped in emphasis. fn is_link_with_optional_emphasis(s: &str) -> bool { @@ -491,4 +592,165 @@ // Link followed by text assert!(!is_standalone_link_or_image_line("[link](url) extra text")); } + + // --- is_html_only_line tests --- + + #[test] + fn test_html_only_badge_line() { + // The reported case: badge with nested <a> and <img> + assert!(is_html_only_line( + r#"<a href="https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook"><img alt="badge" src="https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook/shield"/></a>"# + )); + } + + #[test] + fn test_html_only_self_closing_tags() { + assert!(is_html_only_line( + r#"<img src="https://example.com/image.png" alt="screenshot" width="800" height="600"/>"# + )); + assert!(is_html_only_line(r#"<br/>"#)); + assert!(is_html_only_line(r#"<hr />"#)); + } + + #[test] + fn test_html_only_multiple_tags() { + // Multiple adjacent tags with no text between them + assert!(is_html_only_line(r#"<img src="a.png"/><img src="b.png"/>"#)); + assert!(is_html_only_line(r#"<br/><br/><br/>"#)); + } + + #[test] + fn test_html_only_empty_element() { + // Tags with no content between opening and closing + assert!(is_html_only_line(r#"<video src="long-url.mp4" controls></video>"#)); + assert!(is_html_only_line(r#"<div></div>"#)); + } + + #[test] + fn test_html_only_with_whitespace_between_tags() { + assert!(is_html_only_line(r#"<img src="a.png"/> <img src="b.png"/>"#)); + } + + #[test] + fn test_html_only_quoted_angle_brackets() { + // Attribute value containing > should not break parsing + assert!(is_html_only_line(r#"<img alt="a > b" src="test.png"/>"#)); + assert!(is_html_only_line(r#"<img alt='a > b' src="test.png"/>"#)); + } + + #[test] + fn test_html_only_in_blockquote() { + assert!(is_html_only_line(r#"> <img src="long-url.png" alt="screenshot"/>"#)); + assert!(is_html_only_line(r#">> <a href="url"><img src="img"/></a>"#)); + } + + #[test] + fn test_html_only_in_list() { + assert!(is_html_only_line(r#"- <img src="long-url.png" alt="screenshot"/>"#)); + assert!(is_html_only_line(r#"1. <a href="url"><img src="img"/></a>"#)); + assert!(is_html_only_line(r#" - <img src="long-url.png"/>"#)); + } + + #[test] + fn test_html_only_link_with_text_and_url() { + // <a href="url">text</a> is functionally identical to [text](url) + // which is already exempt — so this should also be exempt + assert!(is_html_only_line( + r#"<a href="https://example.com/very-long-path">Click here for details</a>"# + )); + // With target attribute (reason to use HTML over markdown) + assert!(is_html_only_line( + r#"<a href="https://example.com/very-long-path" target="_blank">Click here for details</a>"# + )); + // Multiple URL attributes + assert!(is_html_only_line( + r#"<a href="https://example.com/path"><img src="https://example.com/badge.svg" alt="status"/></a>"# + )); + } + + #[test] + fn test_not_html_only_text_before_tags() { + assert!(!is_html_only_line(r#"Click here: <a href="url">link</a>"#)); + assert!(!is_html_only_line(r#"See <img src="url"/> for details"#)); + } + + #[test] + fn test_not_html_only_text_after_tags() { + assert!(!is_html_only_line(r#"<a href="url">link</a> - click above"#)); + assert!(!is_html_only_line(r#"<img src="url"/> is an image"#)); + } + + #[test] + fn test_not_html_only_formatting_tags_without_urls() { + // Formatting tags without URL attributes should NOT be exempt — + // the line is long because of text content, not URLs + assert!(!is_html_only_line( + r#"<b>This is very long bold text that exceeds the line length limit</b>"# + )); + assert!(!is_html_only_line( + r#"<p>This is a very long paragraph written in HTML tags for some reason</p>"# + )); + assert!(!is_html_only_line( + r#"<span style="color:red">Some styled text that is quite long</span>"# + )); + assert!(!is_html_only_line( + r#"<em>Emphasized text that goes on and on and on</em>"# + )); + // Multiple formatting tags with text between them + assert!(!is_html_only_line(r#"<b>bold</b> and <i>italic</i>"#)); + } + + #[test] + fn test_not_html_only_plain_text() { + assert!(!is_html_only_line("Just some long text without any HTML")); + assert!(!is_html_only_line("")); + assert!(!is_html_only_line(" ")); + } + + #[test] + fn test_not_html_only_incomplete_tag() { + // Unclosed tag with no complete tag + assert!(!is_html_only_line("<unclosed")); + // Doesn't end with > (unclosed outer element) + assert!(!is_html_only_line(r#"<a href="url">text"#)); + } + + #[test] + fn test_html_only_comment() { + // Simple HTML comments (no > inside) are detected as all-inside-tags + assert!(is_html_only_line( + "<!-- this is a long HTML comment that spans many characters -->" + )); + } + + #[test] + fn test_html_only_media_elements() { + assert!(is_html_only_line( + r#"<video src="https://example.com/very-long-path/video.mp4" poster="https://example.com/thumb.jpg" controls></video>"# + )); + assert!(is_html_only_line( + r#"<audio src="https://example.com/very-long-path/audio.mp3" controls></audio>"# + )); + assert!(is_html_only_line( + r#"<source srcset="https://example.com/image-large.webp" media="(min-width: 800px)"/>"# + )); + assert!(is_html_only_line( + r#"<picture><source srcset="large.webp"/><img src="fallback.png"/></picture>"# + )); + } + + #[test] + fn test_html_only_in_list_with_url_text() { + // List item containing an HTML link with text — should be exempt + assert!(is_html_only_line( + r#"- <a href="https://example.com/very-long-path">documentation link</a>"# + )); + } + + #[test] + fn test_html_only_in_blockquote_with_url_text() { + assert!(is_html_only_line( + r#"> <a href="https://example.com/very-long-path">documentation link</a>"# + )); + } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.53/src/rules/md013_line_length/mod.rs new/rumdl-0.1.54/src/rules/md013_line_length/mod.rs --- old/rumdl-0.1.53/src/rules/md013_line_length/mod.rs 2026-03-16 16:29:34.000000000 +0100 +++ new/rumdl-0.1.54/src/rules/md013_line_length/mod.rs 2026-03-18 17:23:42.000000000 +0100 @@ -22,8 +22,8 @@ pub mod md013_config; use crate::utils::is_template_directive_only; use helpers::{ - extract_list_marker_and_content, has_hard_break, is_github_alert_marker, is_horizontal_rule, is_list_item, - is_standalone_link_or_image_line, split_into_segments, trim_preserving_hard_break, + extract_list_marker_and_content, has_hard_break, is_github_alert_marker, is_horizontal_rule, is_html_only_line, + is_list_item, is_standalone_link_or_image_line, split_into_segments, trim_preserving_hard_break, }; pub use md013_config::MD013Config; use md013_config::{LengthMode, ReflowMode}; @@ -348,6 +348,13 @@ continue; } + // Lines consisting entirely of HTML tags are exempt. + // Badge lines, images with attributes, and similar inline HTML + // are long due to URLs in attributes and can't be meaningfully shortened. + if is_html_only_line(line) { + continue; + } + // Skip setext heading underlines if !line.trim().is_empty() && line.trim().chars().all(|c| c == '=' || c == '-') { continue; @@ -596,6 +603,7 @@ || is_standalone_attr_list(content) || is_snippet_block_delimiter(content) || is_github_alert_marker(trimmed) + || is_html_only_line(content) } fn generate_blockquote_paragraph_fix( @@ -900,6 +908,7 @@ || is_template_directive_only(lines[i]) || is_link_ref_def || ctx.line_info(line_num).is_some_and(|info| info.is_div_marker) + || is_html_only_line(lines[i]) { i += 1; continue; @@ -1111,6 +1120,14 @@ continue; } + // HTML-only lines inside footnotes are not reflowable + if is_html_only_line(next_trimmed) { + fn_lines.push(FnLineType::Verbatim(strip_fn_indent(next), indent)); + last_consumed = i; + i += 1; + continue; + } + // Regular prose content fn_lines.push(FnLineType::Content(next_trimmed.to_string())); last_consumed = i; @@ -1329,11 +1346,12 @@ break; } - // Skip list items, code blocks, headings within containers + // Skip list items, code blocks, headings, HTML-only lines within containers if is_list_item(line.trim()) || line.trim().starts_with("```") || line.trim().starts_with("~~~") || line.trim().starts_with('#') + || is_html_only_line(line) { break; } @@ -2151,6 +2169,10 @@ if !config.strict && is_standalone_link_or_image_line(raw_line) { return true; } + // HTML-only lines: exempt when not strict + if !config.strict && is_html_only_line(raw_line) { + return true; + } false }; @@ -2872,6 +2894,7 @@ || is_standalone_attr_list(next_line) || is_snippet_block_delimiter(next_line) || ctx.line_info(next_line_num).is_some_and(|info| info.is_div_marker) + || is_html_only_line(next_line) { break; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rumdl-0.1.53/src/rules/md013_line_length/tests.rs new/rumdl-0.1.54/src/rules/md013_line_length/tests.rs --- old/rumdl-0.1.53/src/rules/md013_line_length/tests.rs 2026-03-16 16:29:34.000000000 +0100 +++ new/rumdl-0.1.54/src/rules/md013_line_length/tests.rs 2026-03-18 17:23:42.000000000 +0100 @@ -6204,3 +6204,237 @@ "Regular paragraph after blockquote should still warn when blockquotes=false" ); } + +// --- HTML-only line exemption tests (issue #535) --- + +#[test] +fn test_html_only_badge_line_exempt() { + let rule = MD013LineLength::new(80, false, false, false, false); + let content = r#"# Demo + +<a href="https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook"><img alt="badge" src="https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook/shield"/></a>"#; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty(), + "HTML-only badge line should be exempt in non-strict mode, got: {result:?}" + ); +} + +#[test] +fn test_html_only_img_with_attributes_exempt() { + let rule = MD013LineLength::new(80, false, false, false, false); + let content = r#"<img src="https://example.com/very-long-path/to/image.png" alt="screenshot of the application" width="1286" height="185"/>"#; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty(), + "HTML-only img tag should be exempt in non-strict mode, got: {result:?}" + ); +} + +#[test] +fn test_html_only_multiple_badges_exempt() { + let rule = MD013LineLength::new(80, false, false, false, false); + let content = r#"<a href="https://example.com/first"><img src="https://img.shields.io/badge/first-blue"/></a> <a href="https://example.com/second"><img src="https://img.shields.io/badge/second-green"/></a>"#; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty(), + "Multiple HTML badge tags on one line should be exempt, got: {result:?}" + ); +} + +#[test] +fn test_html_only_not_exempt_in_strict_mode() { + let rule = MD013LineLength::new(80, false, false, false, true); + let content = r#"<a href="https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook"><img alt="badge" src="https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook/shield"/></a>"#; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!( + !result.is_empty(), + "HTML-only lines should NOT be exempt in strict mode" + ); +} + +#[test] +fn test_html_with_text_outside_tags_not_exempt() { + let rule = MD013LineLength::new(80, false, false, false, false); + let content = r#"Check out this badge: <a href="https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook"><img alt="badge" src="https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook/shield"/></a>"#; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!( + !result.is_empty(), + "Lines with text outside HTML tags should still be flagged" + ); +} + +#[test] +fn test_html_link_with_text_exempt_like_markdown_link() { + // <a href="url">text</a> is functionally identical to [text](url) + // which is already exempt — HTML links should be exempt too + let rule = MD013LineLength::new(30, false, false, false, false); + let content = r#"<a href="https://example.com/very-long-path">Click here for more information about this topic and read the docs</a>"#; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty(), + "HTML link with text should be exempt (consistent with markdown link exemption), got: {result:?}" + ); +} + +#[test] +fn test_html_formatting_tags_without_urls_not_exempt() { + // <b>, <p>, <em> etc. without URL attributes should still be flagged + let rule = MD013LineLength::new(30, false, false, false, false); + let content = r#"<b>This is very long bold text that definitely exceeds the thirty char limit easily</b>"#; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!( + !result.is_empty(), + "Formatting tags without URL attributes should still be flagged" + ); +} + +#[test] +fn test_html_link_with_target_blank_exempt() { + // HTML used because markdown can't do target="_blank" + let rule = MD013LineLength::new(80, false, false, false, false); + let content = + r#"<a href="https://example.com/very-long-path/to/documentation/page" target="_blank">Documentation</a>"#; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty(), + "HTML link with target=_blank should be exempt, got: {result:?}" + ); +} + +#[test] +fn test_html_only_in_list_exempt() { + let rule = MD013LineLength::new(80, false, false, false, false); + let content = r#"- <a href="https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook"><img alt="badge" src="https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook/shield"/></a>"#; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty(), + "HTML-only line in list item should be exempt, got: {result:?}" + ); +} + +#[test] +fn test_html_only_in_blockquote_exempt() { + let rule = MD013LineLength::new(80, false, false, false, false); + let content = r#"> <a href="https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook"><img alt="badge" src="https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook/shield"/></a>"#; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty(), + "HTML-only line in blockquote should be exempt, got: {result:?}" + ); +} + +#[test] +fn test_html_only_media_elements_exempt() { + let rule = MD013LineLength::new(80, false, false, false, false); + let content = r#"<video src="https://example.com/very-long-path/to/video.mp4" poster="https://example.com/very-long-path/thumb.jpg" controls></video>"#; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty(), + "HTML-only media element should be exempt, got: {result:?}" + ); +} + +#[test] +fn test_html_only_with_quoted_angle_brackets_exempt() { + let rule = MD013LineLength::new(80, false, false, false, false); + let content = r#"<img alt="comparison: value_a > value_b shows the difference clearly in this long alt text" src="https://example.com/image.png"/>"#; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty(), + "HTML tag with > in quoted attribute should be exempt, got: {result:?}" + ); +} + +#[test] +fn test_html_only_reflow_mode_not_merged_into_paragraph() { + // With reflow enabled, HTML-only lines should NOT be merged into adjacent paragraphs. + // This was the actual bug: the reflow path didn't recognize HTML-only lines as + // paragraph boundaries, causing them to be absorbed and flagged. + let config = MD013Config { + line_length: crate::types::LineLength::from_const(80), + reflow: true, + ..Default::default() + }; + let rule = MD013LineLength::from_config_struct(config); + + let content = "Some paragraph text.\n\n<a href=\"https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook\"><img alt=\"badge\" src=\"https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook/shield\"/></a>"; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty(), + "HTML-only line should not generate warnings in reflow mode, got: {result:?}" + ); +} + +#[test] +fn test_html_only_reflow_mode_preserves_html_line() { + // Fix mode should not modify HTML-only lines + let config = MD013Config { + line_length: crate::types::LineLength::from_const(80), + reflow: true, + ..Default::default() + }; + let rule = MD013LineLength::from_config_struct(config); + + let content = "Some paragraph text.\n\n<a href=\"https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook\"><img alt=\"badge\" src=\"https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook/shield\"/></a>\n\nMore text after."; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let fixed = rule.fix(&ctx).unwrap(); + assert!( + fixed.contains(r#"<a href="https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook"><img alt="badge" src="https://dotfyle.com/plugins/chrisgrieser/nvim-rulebook/shield"/></a>"#), + "Fix should preserve HTML-only line unchanged, got:\n{fixed}" + ); +} + +#[test] +fn test_html_link_with_text_reflow_mode_exempt() { + // <a href="url">text</a> should also be exempt in reflow mode + let config = MD013Config { + line_length: crate::types::LineLength::from_const(80), + reflow: true, + ..Default::default() + }; + let rule = MD013LineLength::from_config_struct(config); + + let content = "<a href=\"https://example.com/very-long-path/to/documentation/page\" target=\"_blank\">Click here for documentation</a>"; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let result = rule.check(&ctx).unwrap(); + assert!( + result.is_empty(), + "HTML link with text should be exempt in reflow mode, got: {result:?}" + ); +} + +#[test] +fn test_html_only_adjacent_to_paragraph_not_absorbed() { + // An HTML-only line adjacent to a long paragraph should not be merged + // into that paragraph during reflow + let config = MD013Config { + line_length: crate::types::LineLength::from_const(80), + reflow: true, + ..Default::default() + }; + let rule = MD013LineLength::from_config_struct(config); + + let content = "This is paragraph text that is quite long and exceeds the eighty character limit set for this test case.\n<a href=\"https://example.com\"><img src=\"https://example.com/badge.svg\" alt=\"badge\"/></a>"; + let ctx = LintContext::new(content, MarkdownFlavor::Standard, None); + let fixed = rule.fix(&ctx).unwrap(); + + // The paragraph should be reflowed but the HTML line should be preserved + assert!( + fixed.contains(r#"<a href="https://example.com"><img src="https://example.com/badge.svg" alt="badge"/></a>"#), + "HTML-only line should be preserved during adjacent paragraph reflow, got:\n{fixed}" + ); +} ++++++ rumdl.obsinfo ++++++ --- /var/tmp/diff_new_pack.WPwok3/_old 2026-03-19 17:41:26.791925586 +0100 +++ /var/tmp/diff_new_pack.WPwok3/_new 2026-03-19 17:41:26.863928569 +0100 @@ -1,5 +1,5 @@ name: rumdl -version: 0.1.53 -mtime: 1773674974 -commit: ce2ee387fc15da333da08f395dc578d5eb5e29b7 +version: 0.1.54 +mtime: 1773851022 +commit: 8eeed34c6f8f3bacc43f59a6a34f3c15dd594b20 ++++++ vendor.tar.zst ++++++ /work/SRC/openSUSE:Factory/rumdl/vendor.tar.zst /work/SRC/openSUSE:Factory/.rumdl.new.8177/vendor.tar.zst differ: char 7, line 1
