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-07-10 17:42:55
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/rumdl (Old)
 and      /work/SRC/openSUSE:Factory/.rumdl.new.1991 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "rumdl"

Fri Jul 10 17:42:55 2026 rev:85 rq:1364821 version:0.2.30

Changes:
--------
--- /work/SRC/openSUSE:Factory/rumdl/rumdl.changes      2026-07-03 
16:11:54.263822462 +0200
+++ /work/SRC/openSUSE:Factory/.rumdl.new.1991/rumdl.changes    2026-07-10 
17:45:23.477586177 +0200
@@ -1,0 +2,17 @@
+Fri Jul 10 05:02:48 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 0.2.30:
+  * Added
+    - MD007: clamp explicit fixed-style indent under ordered
+      parents (1f3a32d)
+  * Fixed
+    - lint-context: keep lazy continuation lines in their list
+      block (a17f0af)
+- Update to version 0.2.29:
+  * Fixed
+    - lint-context: keep indented continuation lines when a
+      different list type follows (5dd3c3f)
+    - MD013: treat tabs and extra spaces after a list marker as
+      marker padding (f0f97a2)
+
+-------------------------------------------------------------------

Old:
----
  rumdl-0.2.28.obscpio

New:
----
  rumdl-0.2.30.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ rumdl.spec ++++++
--- /var/tmp/diff_new_pack.ygU0rn/_old  2026-07-10 17:45:31.973877217 +0200
+++ /var/tmp/diff_new_pack.ygU0rn/_new  2026-07-10 17:45:31.977877355 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           rumdl
-Version:        0.2.28
+Version:        0.2.30
 Release:        0
 Summary:        Markdown Linter written in Rust
 License:        MIT

++++++ _service ++++++
--- /var/tmp/diff_new_pack.ygU0rn/_old  2026-07-10 17:45:32.029879136 +0200
+++ /var/tmp/diff_new_pack.ygU0rn/_new  2026-07-10 17:45:32.033879273 +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">refs/tags/v0.2.28</param>
+    <param name="revision">refs/tags/v0.2.30</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.ygU0rn/_old  2026-07-10 17:45:32.061880232 +0200
+++ /var/tmp/diff_new_pack.ygU0rn/_new  2026-07-10 17:45:32.065880370 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param name="url">https://github.com/rvben/rumdl.git</param>
-              <param 
name="changesrevision">c5f50cbbbc790d7adb5583b51e4322ed3dcae65c</param></service></servicedata>
+              <param 
name="changesrevision">12e34feb01c07d62a8161b5ac74940ffe8954c21</param></service></servicedata>
 (No newline at EOF)
 

++++++ rumdl-0.2.28.obscpio -> rumdl-0.2.30.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/.github/workflows/release.yml 
new/rumdl-0.2.30/.github/workflows/release.yml
--- old/rumdl-0.2.28/.github/workflows/release.yml      2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/.github/workflows/release.yml      2026-07-09 
19:39:55.000000000 +0200
@@ -662,10 +662,26 @@
           UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
         run: |
           # Upload only Python packages (wheels and sdist), not Homebrew 
archives.
-          # --check-url skips files already present on PyPI so re-runs stay
-          # idempotent (the equivalent of twine's --skip-existing).
-          uv publish --check-url https://pypi.org/simple/rumdl/ \
-            artifacts/wheel-*/*.whl artifacts/sdist/*.tar.gz
+          # --check-url skips a file only when its bytes already exist on 
PyPI, so
+          # re-running the publish job with the SAME artifacts is idempotent. A
+          # re-run that REBUILT the artifacts produces different bytes (Rust
+          # builds are not reproducible), uv then attempts the upload, and PyPI
+          # rejects the filename collision with "400 File already exists".
+          # Publish per file and tolerate that rejection, the way the crates.io
+          # and npm steps do, so a re-run keeps going and still uploads any 
file
+          # PyPI does not have yet. PyPI keeps the first upload of a filename.
+          status=0
+          for f in artifacts/wheel-*/*.whl artifacts/sdist/*.tar.gz; do
+            if output=$(uv publish --check-url https://pypi.org/simple/rumdl/ 
"$f" 2>&1); then
+              echo "$output"
+            elif echo "$output" | grep -qi "already exists"; then
+              echo "⏭️ $(basename "$f") already on PyPI (first upload wins), 
skipping"
+            else
+              echo "$output"
+              status=1
+            fi
+          done
+          exit $status
 
       - name: Test PyPI upload (dry run)
         if: ${{ inputs.dry_run == true && inputs.skip_pypi != true }}
@@ -694,10 +710,14 @@
           node-version: '24'
           registry-url: 'https://registry.npmjs.org'
 
+      # Pinned, not @latest: npm 12.0.0 ships a provenance path that fails with
+      # "Cannot find module 'sigstore'", so `npm publish --provenance` aborts.
+      # An unpinned upgrade puts every release at the mercy of the newest npm.
+      # 11.18.0 is the last 11.x and supports the OIDC publishing this needs.
       - name: Upgrade npm for OIDC support
         if: ${{ inputs.skip_npm != true }}
         run: |
-          npm install -g npm@latest
+          npm install -g [email protected]
           echo "npm version: $(npm --version)"
 
       - name: Publish WASM to npm
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/.lychee.toml 
new/rumdl-0.2.30/.lychee.toml
--- old/rumdl-0.2.28/.lychee.toml       2026-07-03 11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/.lychee.toml       2026-07-09 19:39:55.000000000 +0200
@@ -21,7 +21,11 @@
 # Exclude URLs from checking (regex). npmjs.com returns 403 to automated link
 # checkers due to bot protection; the package page is valid and is linked from
 # the stability docs alongside crates.io and PyPI.
-exclude = ['^https://www\.npmjs\.com/']
+#
+# GitHub returns 404 on /stargazers to non-browser clients, with or without a
+# token; github.com/rust-lang/rust/stargazers behaves identically. The stars
+# badge in README.md links there and resolves fine in a browser.
+exclude = ['^https://www\.npmjs\.com/', '^https://github\.com/.+/stargazers$']
 
 # Timeout per request in seconds
 timeout = 30
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/CHANGELOG.md 
new/rumdl-0.2.30/CHANGELOG.md
--- old/rumdl-0.2.28/CHANGELOG.md       2026-07-03 11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/CHANGELOG.md       2026-07-09 19:39:55.000000000 +0200
@@ -68,6 +68,25 @@
 
 
 
+
+
+## [0.2.30](https://github.com/rvben/rumdl/compare/v0.2.29...v0.2.30) - 
2026-07-09
+
+### Added
+
+- **MD007**: clamp explicit fixed-style indent under ordered parents 
([1f3a32d](https://github.com/rvben/rumdl/commit/1f3a32df48f8983fc49fafbc6d6a00743dda763b))
+
+### Fixed
+
+- **lint-context**: keep lazy continuation lines in their list block 
([a17f0af](https://github.com/rvben/rumdl/commit/a17f0af7b7d9c2ab71b7416472f05bb00f928d96))
+
+## [0.2.29](https://github.com/rvben/rumdl/compare/v0.2.28...v0.2.29) - 
2026-07-09
+
+### Fixed
+
+- **lint-context**: keep indented continuation lines when a different list 
type follows 
([5dd3c3f](https://github.com/rvben/rumdl/commit/5dd3c3fcdfef353ce8f0aaf575e704ce3754d2fc))
+- **MD013**: treat tabs and extra spaces after a list marker as marker padding 
([f0f97a2](https://github.com/rvben/rumdl/commit/f0f97a29a3d768f8728d5650c63ccb1046cc279c))
+
 ## [0.2.28](https://github.com/rvben/rumdl/compare/v0.2.27...v0.2.28) - 
2026-07-03
 
 ### Added
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/Cargo.lock new/rumdl-0.2.30/Cargo.lock
--- old/rumdl-0.2.28/Cargo.lock 2026-07-03 11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/Cargo.lock 2026-07-09 19:39:55.000000000 +0200
@@ -2283,7 +2283,7 @@
 
 [[package]]
 name = "rumdl"
-version = "0.2.28"
+version = "0.2.30"
 dependencies = [
  "anyhow",
  "assert_cmd",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/Cargo.toml new/rumdl-0.2.30/Cargo.toml
--- old/rumdl-0.2.28/Cargo.toml 2026-07-03 11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/Cargo.toml 2026-07-09 19:39:55.000000000 +0200
@@ -1,6 +1,6 @@
 [package]
 name = "rumdl"
-version = "0.2.28"
+version = "0.2.30"
 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.28/README.md new/rumdl-0.2.30/README.md
--- old/rumdl-0.2.28/README.md  2026-07-03 11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/README.md  2026-07-09 19:39:55.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.28
+    rev: v0.2.30
     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.28
+    rev: v0.2.30
     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.28
+    rev: v0.2.30
     hooks:
       - id: rumdl
         args: [--no-exclude]  # Disable all exclude patterns
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/docs/getting-started/quickstart.md 
new/rumdl-0.2.30/docs/getting-started/quickstart.md
--- old/rumdl-0.2.28/docs/getting-started/quickstart.md 2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/docs/getting-started/quickstart.md 2026-07-09 
19:39:55.000000000 +0200
@@ -96,7 +96,7 @@
 ```yaml
 repos:
   - repo: https://github.com/rvben/rumdl-pre-commit
-    rev: v0.2.28  # Use latest version
+    rev: v0.2.30  # Use latest version
     hooks:
       - id: rumdl
 ```
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/docs/global-settings.md 
new/rumdl-0.2.30/docs/global-settings.md
--- old/rumdl-0.2.28/docs/global-settings.md    2026-07-03 11:03:19.000000000 
+0200
+++ new/rumdl-0.2.30/docs/global-settings.md    2026-07-09 19:39:55.000000000 
+0200
@@ -1376,7 +1376,7 @@
 
 ```yaml
 - repo: https://github.com/rvben/rumdl-pre-commit
-  rev: v0.2.28
+  rev: v0.2.30
   hooks:
     - id: rumdl
       args: [--config=.rumdl.toml]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/docs/md007.md 
new/rumdl-0.2.30/docs/md007.md
--- old/rumdl-0.2.28/docs/md007.md      2026-07-03 11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/docs/md007.md      2026-07-09 19:39:55.000000000 +0200
@@ -89,9 +89,12 @@
 
 #### Mixed Lists: Ordered and Unordered
 
-When you have bullets nested under ordered lists, the bullets **always use 
text-aligned** positioning,
-even if you've set a specific `indent` value. This is because ordered list 
markers have variable
-widths (`1.` vs `10.` vs `100.`), making fixed indentation impractical.
+When you have bullets nested under ordered lists and `style` is not explicitly 
`"fixed"`, the
+bullets **use text-aligned** positioning, even if you've set a specific 
`indent` value. This is
+because ordered list markers have variable widths (`1.` vs `10.` vs `100.`), 
making fixed
+indentation impractical. With an explicit `style = "fixed"`, bullets under 
ordered lists are
+checked against the fixed indent clamped to the parent's content column (see 
the `fixed` style
+below).
 
 ```toml
 [MD007]
@@ -148,6 +151,12 @@
 - You prefer strict mathematical multiples
 - Your team uses markdownlint and you want consistency
 
+**Bullets under ordered lists are clamped.** A bullet cannot sit to the left 
of its parent's
+content column without leaving the item, so under an ordered parent the 
expected indent is the
+larger of the fixed value and the parent's content column. With `indent = 2` 
under `1. `, the
+fixed value would be 2 but the content column is 3, so 3 is expected; deeper 
levels build on the
+corrected position of their parent. This keeps autofixes from restructuring 
the document.
+
 #### Example: Difference between styles
 
 With a 3-character marker like `1.`:
@@ -164,8 +173,16 @@
 
 ```markdown
 1. First item
-  * Child at 2 spaces (indent × 1)
-    * Grandchild at 4 spaces (indent × 2)
+   * Child at 3 spaces (fixed 2 clamped to the parent's content column)
+     * Grandchild at 5 spaces (fixed 4 clamped to the child's content column)
+```
+
+**fixed (indent = 4):**
+
+```markdown
+1. First item
+    * Child at 4 spaces (indent × 1, already past the content column)
+        * Grandchild at 8 spaces (indent × 2)
 ```
 
 ### Example with start-indented: true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/docs/mdformat-comparison.md 
new/rumdl-0.2.30/docs/mdformat-comparison.md
--- old/rumdl-0.2.28/docs/mdformat-comparison.md        2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/docs/mdformat-comparison.md        2026-07-09 
19:39:55.000000000 +0200
@@ -232,7 +232,7 @@
 
    # After
    - repo: https://github.com/rvben/rumdl-pre-commit
-     rev: v0.2.28
+     rev: v0.2.30
      hooks:
        - id: rumdl
     ```
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/docs/usage/ci-cd.md 
new/rumdl-0.2.30/docs/usage/ci-cd.md
--- old/rumdl-0.2.28/docs/usage/ci-cd.md        2026-07-03 11:03:19.000000000 
+0200
+++ new/rumdl-0.2.30/docs/usage/ci-cd.md        2026-07-09 19:39:55.000000000 
+0200
@@ -40,7 +40,7 @@
 ```yaml
 - uses: rvben/rumdl@v0
   with:
-    version: "0.2.28"
+    version: "0.2.30"
     path: docs/
 ```
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/docs/usage/pre-commit.md 
new/rumdl-0.2.30/docs/usage/pre-commit.md
--- old/rumdl-0.2.28/docs/usage/pre-commit.md   2026-07-03 11:03:19.000000000 
+0200
+++ new/rumdl-0.2.30/docs/usage/pre-commit.md   2026-07-09 19:39:55.000000000 
+0200
@@ -13,7 +13,7 @@
 ```yaml title=".pre-commit-config.yaml"
 repos:
   - repo: https://github.com/rvben/rumdl-pre-commit
-    rev: v0.2.28  # Use latest version
+    rev: v0.2.30  # Use latest version
     hooks:
       - id: rumdl      # Lint only; add args [--fix] to auto-fix
       - id: rumdl-fmt  # Pure format, always exits 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/npm/cli-darwin-arm64/package.json 
new/rumdl-0.2.30/npm/cli-darwin-arm64/package.json
--- old/rumdl-0.2.28/npm/cli-darwin-arm64/package.json  2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/npm/cli-darwin-arm64/package.json  2026-07-09 
19:39:55.000000000 +0200
@@ -1,6 +1,6 @@
 {
   "name": "@rumdl/cli-darwin-arm64",
-  "version": "0.2.28",
+  "version": "0.2.30",
   "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.28/npm/cli-darwin-x64/package.json 
new/rumdl-0.2.30/npm/cli-darwin-x64/package.json
--- old/rumdl-0.2.28/npm/cli-darwin-x64/package.json    2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/npm/cli-darwin-x64/package.json    2026-07-09 
19:39:55.000000000 +0200
@@ -1,6 +1,6 @@
 {
   "name": "@rumdl/cli-darwin-x64",
-  "version": "0.2.28",
+  "version": "0.2.30",
   "description": "rumdl binary for macOS x64 (Intel)",
   "license": "MIT",
   "repository": {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/npm/cli-linux-arm64/package.json 
new/rumdl-0.2.30/npm/cli-linux-arm64/package.json
--- old/rumdl-0.2.28/npm/cli-linux-arm64/package.json   2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/npm/cli-linux-arm64/package.json   2026-07-09 
19:39:55.000000000 +0200
@@ -1,6 +1,6 @@
 {
   "name": "@rumdl/cli-linux-arm64",
-  "version": "0.2.28",
+  "version": "0.2.30",
   "description": "rumdl binary for Linux ARM64 (glibc)",
   "license": "MIT",
   "repository": {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/npm/cli-linux-arm64-musl/package.json 
new/rumdl-0.2.30/npm/cli-linux-arm64-musl/package.json
--- old/rumdl-0.2.28/npm/cli-linux-arm64-musl/package.json      2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/npm/cli-linux-arm64-musl/package.json      2026-07-09 
19:39:55.000000000 +0200
@@ -1,6 +1,6 @@
 {
   "name": "@rumdl/cli-linux-arm64-musl",
-  "version": "0.2.28",
+  "version": "0.2.30",
   "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.28/npm/cli-linux-x64/package.json 
new/rumdl-0.2.30/npm/cli-linux-x64/package.json
--- old/rumdl-0.2.28/npm/cli-linux-x64/package.json     2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/npm/cli-linux-x64/package.json     2026-07-09 
19:39:55.000000000 +0200
@@ -1,6 +1,6 @@
 {
   "name": "@rumdl/cli-linux-x64",
-  "version": "0.2.28",
+  "version": "0.2.30",
   "description": "rumdl binary for Linux x64 (glibc)",
   "license": "MIT",
   "repository": {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/npm/cli-linux-x64-musl/package.json 
new/rumdl-0.2.30/npm/cli-linux-x64-musl/package.json
--- old/rumdl-0.2.28/npm/cli-linux-x64-musl/package.json        2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/npm/cli-linux-x64-musl/package.json        2026-07-09 
19:39:55.000000000 +0200
@@ -1,6 +1,6 @@
 {
   "name": "@rumdl/cli-linux-x64-musl",
-  "version": "0.2.28",
+  "version": "0.2.30",
   "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.28/npm/cli-win32-x64/package.json 
new/rumdl-0.2.30/npm/cli-win32-x64/package.json
--- old/rumdl-0.2.28/npm/cli-win32-x64/package.json     2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/npm/cli-win32-x64/package.json     2026-07-09 
19:39:55.000000000 +0200
@@ -1,6 +1,6 @@
 {
   "name": "@rumdl/cli-win32-x64",
-  "version": "0.2.28",
+  "version": "0.2.30",
   "description": "rumdl binary for Windows x64",
   "license": "MIT",
   "repository": {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/npm/rumdl/package.json 
new/rumdl-0.2.30/npm/rumdl/package.json
--- old/rumdl-0.2.28/npm/rumdl/package.json     2026-07-03 11:03:19.000000000 
+0200
+++ new/rumdl-0.2.30/npm/rumdl/package.json     2026-07-09 19:39:55.000000000 
+0200
@@ -1,6 +1,6 @@
 {
   "name": "rumdl",
-  "version": "0.2.28",
+  "version": "0.2.30",
   "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.28",
-    "@rumdl/cli-darwin-arm64": "0.2.28",
-    "@rumdl/cli-linux-x64": "0.2.28",
-    "@rumdl/cli-linux-arm64": "0.2.28",
-    "@rumdl/cli-linux-x64-musl": "0.2.28",
-    "@rumdl/cli-linux-arm64-musl": "0.2.28",
-    "@rumdl/cli-win32-x64": "0.2.28"
+    "@rumdl/cli-darwin-x64": "0.2.30",
+    "@rumdl/cli-darwin-arm64": "0.2.30",
+    "@rumdl/cli-linux-x64": "0.2.30",
+    "@rumdl/cli-linux-arm64": "0.2.30",
+    "@rumdl/cli-linux-x64-musl": "0.2.30",
+    "@rumdl/cli-linux-arm64-musl": "0.2.30",
+    "@rumdl/cli-win32-x64": "0.2.30"
   }
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/src/lint_context/list_blocks.rs 
new/rumdl-0.2.30/src/lint_context/list_blocks.rs
--- old/rumdl-0.2.28/src/lint_context/list_blocks.rs    2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/src/lint_context/list_blocks.rs    2026-07-09 
19:39:55.000000000 +0200
@@ -358,16 +358,10 @@
                         &mut min_continuation_for_tracking,
                     );
                 } else {
-                    // End current block and start a new one
-                    // When a different list type starts AT THE SAME LEVEL 
(not nested),
-                    // trim back lazy continuation lines
-                    if !same_type
-                        && !is_nested
-                        && let Some(&last_item) = block.item_lines.last()
-                    {
-                        block.end_line = last_item;
-                    }
-
+                    // End current block and start a new one. The block keeps 
every
+                    // continuation line it collected, including lazy (column 
0) ones:
+                    // per CommonMark those continue the item's paragraph, so 
cutting
+                    // them off would make MD032 insert its blank line inside 
the item.
                     let new_block = ListBlock {
                         start_line: line_num,
                         end_line: line_num,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/src/rules/md007_ul_indent.rs 
new/rumdl-0.2.30/src/rules/md007_ul_indent.rs
--- old/rumdl-0.2.28/src/rules/md007_ul_indent.rs       2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/src/rules/md007_ul_indent.rs       2026-07-09 
19:39:55.000000000 +0200
@@ -117,10 +117,10 @@
     fn terminate_closed_items(
         ctx: &crate::lint_context::LintContext,
         line_info: &crate::lint_context::LineInfo,
-        list_stack: &mut Vec<(usize, usize, bool, usize, usize, bool)>,
+        list_stack: &mut Vec<(usize, usize, bool, usize, usize, bool, usize)>,
         line_bq_depth: usize,
     ) {
-        while let Some(&(_, _, _, content_col, item_bq_depth, _)) = 
list_stack.last() {
+        while let Some(&(_, _, _, content_col, item_bq_depth, _, _)) = 
list_stack.last() {
             let closed = match item_bq_depth.cmp(&line_bq_depth) {
                 // The line has exited a deeper blockquote the item lived in.
                 std::cmp::Ordering::Greater => true,
@@ -230,7 +230,7 @@
 
     fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
         let mut warnings = Vec::new();
-        let mut list_stack: Vec<(usize, usize, bool, usize, usize, bool)> = 
Vec::new(); // Stack of (marker_visual_col, line_num, is_ordered, 
content_visual_col, blockquote_depth, exempt) for tracking nesting. `exempt` 
marks an unordered item that inherited the ordered-ancestor MD007 exemption.
+        let mut list_stack: Vec<(usize, usize, bool, usize, usize, bool, 
usize)> = Vec::new(); // Stack of (marker_visual_col, line_num, is_ordered, 
content_visual_col, blockquote_depth, chain, source_content_visual_col) for 
tracking nesting. `chain` marks an unordered item that inherited the 
ordered-ancestor MD007 exemption or was checked under the fixed-style clamp. 
`content_visual_col` is the corrected (post-fix) column that expectation math 
builds on; `source_content_visual_col` is the column as written, which 
containment is judged against.
 
         for (line_idx, line_info) in ctx.lines.iter().enumerate() {
             // Skip if this line is in a code block, front matter, or 
mkdocstrings
@@ -356,7 +356,7 @@
 
                 // Clean up stack - remove items at same or deeper indentation,
                 // but only consider items at the same blockquote depth
-                while let Some(&(indent, _, _, _, item_bq_depth, _)) = 
list_stack.last() {
+                while let Some(&(indent, _, _, _, item_bq_depth, _, _)) = 
list_stack.last() {
                     if item_bq_depth == bq_depth && indent >= 
visual_marker_for_nesting {
                         list_stack.pop();
                     } else if item_bq_depth > bq_depth {
@@ -379,7 +379,7 @@
                 // A deeper quote indented into the ancestor's content is part 
of that
                 // item and keeps it open. Same-depth nesting and items 
already inside a
                 // blockquote are left to the loop above and the exemption 
check below.
-                while let Some(&(_, _, _, content_col, item_bq_depth, _)) = 
list_stack.last() {
+                while let Some(&(_, _, _, content_col, item_bq_depth, _, _)) = 
list_stack.last() {
                     if item_bq_depth < bq_depth
                         && content_col > Self::indent_relative_to_depth(ctx, 
line_info, item_bq_depth)
                     {
@@ -400,6 +400,7 @@
                         visual_content_column,
                         bq_depth,
                         false,
+                        visual_content_column,
                     ));
                     continue;
                 }
@@ -430,12 +431,44 @@
                 let threshold_ok = list_stack
                     .iter()
                     .any(|item| item.4 == bq_depth && item.2 && item.3 <= 
visual_marker_column);
+                // The marker-based cleanup above keeps a list item whose 
marker sits
+                // between the previous item's marker and its content column, 
even
+                // though per CommonMark such an item is the previous one's 
sibling,
+                // not its child. Harmless under the exemption, but the clamp 
would
+                // then resolve against the too-deep parent and its fix would 
rewrite
+                // the sibling into a child (and contradict MD005). Before 
judging the
+                // chain, pop every trailing item whose source content column 
this
+                // marker sits left of, ordered ones included: with nested 
ordered
+                // lists, threshold_ok can hold via an outer ordered ancestor 
while
+                // the innermost ordered item does not contain the marker at 
all.
+                if ctx.flavor != crate::config::MarkdownFlavor::MkDocs
+                    && threshold_ok
+                    && self.config.style_explicit
+                    && self.config.style == md007_config::IndentStyle::Fixed
+                {
+                    while let Some(&(_, _, _, _, item_bq_depth, _, 
source_content_col)) = list_stack.last() {
+                        if item_bq_depth == bq_depth && source_content_col > 
visual_marker_column {
+                            list_stack.pop();
+                        } else {
+                            break;
+                        }
+                    }
+                }
                 let chain_ok = list_stack
                     .iter()
                     .rev()
                     .find(|item| item.4 == bq_depth)
                     .is_some_and(|item| item.2 || item.5);
-                if ctx.flavor != crate::config::MarkdownFlavor::MkDocs && 
threshold_ok && chain_ok {
+                let ordered_chain = ctx.flavor != 
crate::config::MarkdownFlavor::MkDocs && threshold_ok && chain_ok;
+                // An explicit style = "fixed" opts out of the exemption: the 
item is
+                // checked against the fixed expectation, clamped below to the 
parent's
+                // content column. The clamp is what makes checking safe here: 
a bullet
+                // left of its parent's content column leaves the item, so an 
unclamped
+                // fixed expectation would let the fix restructure the 
document.
+                let clamp_to_parent = ordered_chain
+                    && self.config.style_explicit
+                    && self.config.style == md007_config::IndentStyle::Fixed;
+                if ordered_chain && !clamp_to_parent {
                     list_stack.push((
                         visual_marker_column,
                         line_idx,
@@ -443,6 +476,7 @@
                         visual_content_column,
                         bq_depth,
                         true,
+                        visual_content_column,
                     ));
                     continue;
                 }
@@ -455,7 +489,7 @@
                     .iter()
                     .rev()
                     .find(|item| item.4 == bq_depth)
-                    .map(|&(_, _, is_ordered, content_col, _, _)| (is_ordered, 
content_col));
+                    .map(|&(_, _, is_ordered, content_col, _, _, _)| 
(is_ordered, content_col));
 
                 // Calculate expected indent using per-parent logic
                 // When start_indented is true, only depth-0 items use the 
start_indent value.
@@ -468,21 +502,33 @@
                     self.calculate_expected_indent(nesting_level, parent_info)
                 };
 
+                // Clamp the fixed expectation to the parent's content column. 
The
+                // floor cascades: a fixed child stores its corrected content 
column
+                // on the stack, so a grandchild's floor is measured from 
where the
+                // fix puts the child, not from the source.
+                if clamp_to_parent && let Some((_, parent_content_col)) = 
parent_info {
+                    expected_indent = expected_indent.max(parent_content_col);
+                }
+
                 // When indent is explicitly set and parent is ordered, also 
accept
                 // the fixed indent value (nesting_level * indent). This lets 
users
                 // choose either text-aligned or their configured indent under 
ordered lists.
-                let also_acceptable =
-                    if self.config.indent_explicit && 
parent_info.is_some_and(|(is_ordered, _)| is_ordered) {
-                        Some(nesting_level * self.config.indent.get() as usize)
-                    } else {
-                        None
-                    };
+                // Not under the clamp: there the fixed value can sit left of 
the
+                // parent's content column, and suggesting it would detach the 
item.
+                let also_acceptable = if !clamp_to_parent
+                    && self.config.indent_explicit
+                    && parent_info.is_some_and(|(is_ordered, _)| is_ordered)
+                {
+                    Some(nesting_level * self.config.indent.get() as usize)
+                } else {
+                    None
+                };
 
                 // MkDocs (Python-Markdown) uses 4-space-tab continuation for 
list items.
                 // Under an ordered list item, Python-Markdown requires at 
least
                 // marker_column + 4 spaces for continuation content to be 
recognized.
                 if ctx.flavor == crate::config::MarkdownFlavor::MkDocs
-                    && let Some(&(parent_marker_col, _, true, _, _, _)) =
+                    && let Some(&(parent_marker_col, _, true, _, _, _, _)) =
                         list_stack.iter().rev().find(|item| item.4 == bq_depth 
&& item.2)
                 {
                     expected_indent = expected_indent.max(parent_marker_col + 
4);
@@ -509,13 +555,18 @@
                 // stored value is unchanged.
                 let marker_width = 
visual_content_column.saturating_sub(visual_marker_column);
                 let expected_content_visual_col = accepted_indent + 
marker_width;
+                // A clamped item stays part of the ordered-rooted chain: its 
own
+                // descendants must resolve through the chain flag exactly as 
they
+                // would have through an exempt ancestor, or they would fall 
back to
+                // the unclamped fixed expectation and detach.
                 list_stack.push((
                     visual_marker_column,
                     line_idx,
                     false,
                     expected_content_visual_col,
                     bq_depth,
-                    false,
+                    clamp_to_parent,
+                    visual_content_column,
                 ));
 
                 // A top-level item (depth 0) is expected at column 0 when 
start_indented
@@ -2488,6 +2539,284 @@
         );
     }
 
+    // An explicit style = "fixed" under an ordered parent clamps instead of
+    // exempting: the expectation is max(nesting_level * indent, parent content
+    // column). A bullet cannot sit left of its parent's content column without
+    // leaving the item, so the floor keeps the fix from walking an item out of
+    // its list; the ISSUE_638_INPUT guard above stays green because 3 and 5 
are
+    // exactly the clamped expectations for indent = 2.
+    fn fixed_style_rule(indent: u8) -> MD007ULIndent {
+        MD007ULIndent::from_config_struct(MD007Config {
+            indent: crate::types::IndentSize::from_const(indent),
+            start_indented: false,
+            start_indent: crate::types::IndentSize::from_const(2),
+            style: md007_config::IndentStyle::Fixed,
+            style_explicit: true,
+            indent_explicit: true,
+        })
+    }
+
+    #[test]
+    fn test_fixed_style_clamp_flags_over_indented_bullet_under_ordered() {
+        let rule = fixed_style_rule(2);
+        let content = "1. Some text\n    - four spaces\n";
+        let ctx = LintContext::new(content, 
crate::config::MarkdownFlavor::Standard, None);
+        let result = rule.check(&ctx).unwrap();
+        assert_eq!(
+            result.len(),
+            1,
+            "a bullet at 4 under a content column of 3 is flagged: {result:?}"
+        );
+        assert!(
+            result[0].message.contains("Expected 3") && 
result[0].message.contains("found 4"),
+            "clamped expectation is the parent content column, got: {}",
+            result[0].message
+        );
+        let fixed = rule.fix(&ctx).unwrap();
+        assert_eq!(fixed, "1. Some text\n   - four spaces\n");
+    }
+
+    #[test]
+    fn test_fixed_style_clamp_accepts_bullet_at_parent_content_column() {
+        let rule = fixed_style_rule(2);
+        let content = "1. Some text\n   - three spaces\n";
+        let ctx = LintContext::new(content, 
crate::config::MarkdownFlavor::Standard, None);
+        let result = rule.check(&ctx).unwrap();
+        assert!(result.is_empty(), "the clamped expectation itself passes: 
{result:?}");
+    }
+
+    #[test]
+    fn test_fixed_style_clamp_pulls_five_spaces_to_content_column() {
+        let rule = fixed_style_rule(2);
+        let content = "1. Some text\n     - five spaces\n";
+        let ctx = LintContext::new(content, 
crate::config::MarkdownFlavor::Standard, None);
+        let result = rule.check(&ctx).unwrap();
+        assert_eq!(result.len(), 1, "{result:?}");
+        let fixed = rule.fix(&ctx).unwrap();
+        assert_eq!(fixed, "1. Some text\n   - five spaces\n");
+    }
+
+    #[test]
+    fn test_fixed_style_clamp_cascades_through_nested_bullets() {
+        // The grandchild's floor is the (fixed) child's content column: the
+        // child sits at the clamped 3, its content starts at 5, so fixed 4 is
+        // clamped to 5 and the source's 6 is pulled in.
+        let rule = fixed_style_rule(2);
+        let content = "1. Ordered\n   - child\n      - grandchild\n";
+        let ctx = LintContext::new(content, 
crate::config::MarkdownFlavor::Standard, None);
+        let result = rule.check(&ctx).unwrap();
+        assert_eq!(result.len(), 1, "only the grandchild is off: {result:?}");
+        assert!(
+            result[0].message.contains("Expected 5") && 
result[0].message.contains("found 6"),
+            "got: {}",
+            result[0].message
+        );
+        let fixed = rule.fix(&ctx).unwrap();
+        assert_eq!(fixed, "1. Ordered\n   - child\n     - grandchild\n");
+        let refixed_ctx = LintContext::new(&fixed, 
crate::config::MarkdownFlavor::Standard, None);
+        assert!(rule.check(&refixed_ctx).unwrap().is_empty(), "fix is stable");
+    }
+
+    #[test]
+    fn test_fixed_style_clamp_respects_wider_fixed_indent() {
+        // With indent = 4 the fixed expectation (4) is already at or past the
+        // parent content column (3), so the clamp is inert and fixed style 
wins.
+        let rule = fixed_style_rule(4);
+        let content = "1. Some text\n   - three spaces\n";
+        let ctx = LintContext::new(content, 
crate::config::MarkdownFlavor::Standard, None);
+        let result = rule.check(&ctx).unwrap();
+        assert_eq!(result.len(), 1, "{result:?}");
+        assert!(
+            result[0].message.contains("Expected 4") && 
result[0].message.contains("found 3"),
+            "got: {}",
+            result[0].message
+        );
+        let fixed = rule.fix(&ctx).unwrap();
+        assert_eq!(fixed, "1. Some text\n    - three spaces\n");
+    }
+
+    #[test]
+    fn test_fixed_style_clamp_uses_measured_content_column_of_wide_marker() {
+        // An ol-align-column style source pads the ordered marker, so the
+        // content column is 4; the floor follows the measured column.
+        let rule = fixed_style_rule(2);
+        let content = "1.  Some text\n     - five spaces\n";
+        let ctx = LintContext::new(content, 
crate::config::MarkdownFlavor::Standard, None);
+        let result = rule.check(&ctx).unwrap();
+        assert_eq!(result.len(), 1, "{result:?}");
+        assert!(
+            result[0].message.contains("Expected 4") && 
result[0].message.contains("found 5"),
+            "got: {}",
+            result[0].message
+        );
+        let fixed = rule.fix(&ctx).unwrap();
+        assert_eq!(fixed, "1.  Some text\n    - five spaces\n");
+
+        let ok = "1.  Some text\n    - four spaces\n";
+        let ok_ctx = LintContext::new(ok, 
crate::config::MarkdownFlavor::Standard, None);
+        assert!(rule.check(&ok_ctx).unwrap().is_empty());
+    }
+
+    #[test]
+    fn test_fixed_style_clamp_in_blockquote() {
+        let rule = fixed_style_rule(2);
+        let content = "> 1. Some text\n>     - four spaces\n";
+        let ctx = LintContext::new(content, 
crate::config::MarkdownFlavor::Standard, None);
+        let result = rule.check(&ctx).unwrap();
+        assert_eq!(result.len(), 1, "{result:?}");
+        assert!(
+            result[0].message.contains("Expected 3") && 
result[0].message.contains("found 4"),
+            "got: {}",
+            result[0].message
+        );
+        let fixed = rule.fix(&ctx).unwrap();
+        assert_eq!(fixed, "> 1. Some text\n>    - four spaces\n");
+    }
+
+    #[test]
+    fn test_fixed_style_clamp_treats_near_sibling_as_sibling() {
+        // The bullet at 4 sits left of the previous bullet's content column
+        // (5), so per CommonMark it is that bullet's sibling, not its child.
+        // The clamp must resolve it against the ordered parent (expected 3,
+        // agreeing with MD005), not against the deeper bullet (5): a fix to 5
+        // would rewrite the sibling into a child.
+        let rule = fixed_style_rule(2);
+        let content = "1. x\n   - a\n    - b\n";
+        let ctx = LintContext::new(content, 
crate::config::MarkdownFlavor::Standard, None);
+        let result = rule.check(&ctx).unwrap();
+        assert_eq!(result.len(), 1, "{result:?}");
+        assert!(
+            result[0].message.contains("Expected 3") && 
result[0].message.contains("found 4"),
+            "near-sibling resolves against the ordered parent, got: {}",
+            result[0].message
+        );
+        let fixed = rule.fix(&ctx).unwrap();
+        assert_eq!(fixed, "1. x\n   - a\n   - b\n");
+    }
+
+    #[test]
+    fn test_fixed_style_clamp_child_after_near_sibling_resolves_against_it() {
+        // b is a's sibling (marker 4 < a's content 5, then fixed to 3); c at 6
+        // is inside b's content and is its child, so c's floor is b's 
corrected
+        // content column.
+        let rule = fixed_style_rule(2);
+        let content = "1. x\n   - a\n    - b\n      - c\n";
+        let ctx = LintContext::new(content, 
crate::config::MarkdownFlavor::Standard, None);
+        let result = rule.check(&ctx).unwrap();
+        assert_eq!(result.len(), 2, "b and c are both off: {result:?}");
+        assert!(
+            result[0].message.contains("Expected 3") && 
result[0].message.contains("found 4"),
+            "got: {}",
+            result[0].message
+        );
+        assert!(
+            result[1].message.contains("Expected 5") && 
result[1].message.contains("found 6"),
+            "got: {}",
+            result[1].message
+        );
+        let fixed = rule.fix(&ctx).unwrap();
+        assert_eq!(fixed, "1. x\n   - a\n   - b\n     - c\n");
+    }
+
+    #[test]
+    fn test_fixed_style_clamp_pops_near_sibling_of_over_indented_bullet() {
+        // Containment is judged against a's SOURCE content column (6), so b at
+        // 5 is a's sibling even though a itself gets pulled to 3. Both land at
+        // 3; resolving b against a's corrected column would leave b at 5 and
+        // silently turn the sibling into a child once a moves.
+        let rule = fixed_style_rule(2);
+        let content = "1. x\n    - a\n     - b\n";
+        let ctx = LintContext::new(content, 
crate::config::MarkdownFlavor::Standard, None);
+        let result = rule.check(&ctx).unwrap();
+        assert_eq!(result.len(), 2, "a and b are both flagged: {result:?}");
+        assert!(
+            result[1].message.contains("Expected 3") && 
result[1].message.contains("found 5"),
+            "b resolves against the ordered parent, got: {}",
+            result[1].message
+        );
+        let fixed = rule.fix(&ctx).unwrap();
+        assert_eq!(fixed, "1. x\n   - a\n   - b\n");
+    }
+
+    #[test]
+    fn test_fixed_style_clamp_keeps_child_of_over_indented_bullet() {
+        // c at 7 is inside a's source content column (6), so it stays a's
+        // child and follows a to the corrected floor.
+        let rule = fixed_style_rule(2);
+        let content = "1. x\n    - a\n       - c\n";
+        let ctx = LintContext::new(content, 
crate::config::MarkdownFlavor::Standard, None);
+        let result = rule.check(&ctx).unwrap();
+        assert_eq!(result.len(), 2, "a and c are both flagged: {result:?}");
+        assert!(
+            result[1].message.contains("Expected 5") && 
result[1].message.contains("found 7"),
+            "c's floor is a's corrected content column, got: {}",
+            result[1].message
+        );
+        let fixed = rule.fix(&ctx).unwrap();
+        assert_eq!(fixed, "1. x\n   - a\n     - c\n");
+    }
+
+    #[test]
+    fn test_fixed_style_clamp_pops_ordered_near_sibling() {
+        // b at 6 sits between the nested ordered item's marker (5) and its
+        // content column (8), so b is that item's sibling: a child of a, whose
+        // corrected content column (5) is its floor. Resolving b against the
+        // nested ordered item would fix it to 8 and rewrite the sibling into
+        // a child.
+        let rule = fixed_style_rule(2);
+        let content = "1. root\n   - a\n     1. sub\n      - b\n";
+        let ctx = LintContext::new(content, 
crate::config::MarkdownFlavor::Standard, None);
+        let result = rule.check(&ctx).unwrap();
+        assert_eq!(result.len(), 1, "only b is off: {result:?}");
+        assert!(
+            result[0].message.contains("Expected 5") && 
result[0].message.contains("found 6"),
+            "b resolves against a, not the nested ordered sibling, got: {}",
+            result[0].message
+        );
+        let fixed = rule.fix(&ctx).unwrap();
+        assert_eq!(fixed, "1. root\n   - a\n     1. sub\n     - b\n");
+    }
+
+    #[test]
+    fn test_fixed_style_clamp_leaves_sibling_bullet_left_of_content_column() {
+        // A bullet left of the ordered content column is a sibling list, not a
+        // sublist. The plain fixed expectation applies, 2 matches it, and no
+        // fix ever moves a sibling right into the item (the #638 failure 
mode).
+        let rule = fixed_style_rule(2);
+        let content = "1. Some text\n  - two spaces\n";
+        let ctx = LintContext::new(content, 
crate::config::MarkdownFlavor::Standard, None);
+        let result = rule.check(&ctx).unwrap();
+        assert!(
+            result.is_empty(),
+            "sibling bullet at the fixed indent stays silent: {result:?}"
+        );
+    }
+
+    #[test]
+    fn test_fixed_style_clamp_requires_explicit_style() {
+        // indent_explicit alone keeps the ordered-ancestor exemption; only an
+        // explicit style = "fixed" opts into the clamp.
+        let config = MD007Config {
+            indent: crate::types::IndentSize::from_const(2),
+            start_indented: false,
+            start_indent: crate::types::IndentSize::from_const(2),
+            style: md007_config::IndentStyle::TextAligned,
+            style_explicit: false,
+            indent_explicit: true,
+        };
+        let rule = MD007ULIndent::from_config_struct(config);
+        let content = "1. Some text\n    - four spaces\n";
+        let ctx = LintContext::new(content, 
crate::config::MarkdownFlavor::Standard, None);
+        let result = rule.check(&ctx).unwrap();
+        assert!(result.is_empty(), "no explicit style, exemption stays: 
{result:?}");
+
+        let smart = MD007ULIndent::new(2);
+        assert!(
+            smart.check(&ctx).unwrap().is_empty(),
+            "smart default keeps the exemption too"
+        );
+    }
+
     #[test]
     fn test_issue_638_deeper_unordered_chain_under_ordered() {
         // Every unordered item below the ordered ancestor is exempt, at any 
depth.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/src/rules/md013_line_length/helpers.rs 
new/rumdl-0.2.30/src/rules/md013_line_length/helpers.rs
--- old/rumdl-0.2.28/src/rules/md013_line_length/helpers.rs     2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/src/rules/md013_line_length/helpers.rs     2026-07-09 
19:39:55.000000000 +0200
@@ -112,6 +112,91 @@
     segments
 }
 
+/// GFM task list checkboxes, including the space that separates them from the 
item text.
+const TASK_CHECKBOXES: [&str; 3] = ["[ ] ", "[x] ", "[X] "];
+
+/// Whitespace that separates a list marker from the item text. CommonMark
+/// expands tabs, so a tab is marker padding rather than content, and rumdl's
+/// own parser accepts `-\tfoo` as a list item.
+const MARKER_PADDING: [char; 2] = [' ', '\t'];
+
+/// Strip a GFM task list checkbox from the text that follows a list marker.
+///
+/// Authors may pad between the marker and the checkbox (`-   [ ] task`). That
+/// padding belongs to the marker, so it is skipped before matching. Without
+/// this the checkbox reaches the reflow engine as prose, which normalizes the
+/// space inside it and rewrites `[ ]` as `[]`, silently turning a task item
+/// into plain text.
+///
+/// Returns the checkbox and the item text after it, or `None` when the item
+/// carries no checkbox.
+fn strip_task_checkbox(after_marker: &str) -> Option<(&'static str, &str)> {
+    let content = after_marker.trim_start_matches(MARKER_PADDING);
+    TASK_CHECKBOXES
+        .iter()
+        .find_map(|checkbox| content.strip_prefix(checkbox).map(|text| 
(*checkbox, text)))
+}
+
+/// Display width of `s`, expanding tabs to CommonMark's four-column tab stops.
+fn display_width(s: &str) -> usize {
+    s.chars()
+        .fold(0, |col, c| if c == '\t' { col + 4 - col % 4 } else { col + 1 })
+}
+
+/// The source marker of a list item: the text before the item's content, 
exactly as
+/// written, plus the column at which that content begins.
+///
+/// This is distinct from the marker that [`extract_list_marker_and_content`] 
returns.
+/// That one is *normalized* to a single space and is what gets re-emitted; 
this one
+/// describes the source and is what indentation arithmetic must measure 
against. A
+/// reflowed item's nested blocks move by `new_content_column - 
source.content_col`,
+/// so measuring the shift against the normalized marker silently corrupts any 
item
+/// whose author wrote more than one space after the marker.
+///
+/// `content_col` is a display column, so a tab in the padding counts as the 
columns
+/// it actually occupies rather than its single byte.
+#[derive(Debug, PartialEq, Eq)]
+pub(crate) struct SourceMarker {
+    pub text: String,
+    pub content_col: usize,
+}
+
+/// Returns `None` when the line does not open a list item.
+pub(crate) fn source_list_marker(line: &str) -> Option<SourceMarker> {
+    let indent_len = line.len() - line.trim_start().len();
+    let trimmed = &line[indent_len..];
+
+    let after_marker = if let Some(rest) = trimmed.strip_prefix(['-', '*', 
'+']) {
+        rest
+    } else {
+        // Ordered marker: digits then '.'
+        let digits = trimmed.find('.')?;
+        if digits == 0 || !trimmed[..digits].chars().all(|c| 
c.is_ascii_digit()) {
+            return None;
+        }
+        &trimmed[digits + 1..]
+    };
+
+    // A marker must be followed by padding.
+    if !after_marker.starts_with(MARKER_PADDING) {
+        return None;
+    }
+    let after_padding = after_marker.trim_start_matches(MARKER_PADDING);
+    // The checkbox is glued to the marker so reflow never rewrites it, 
matching the
+    // width that `extract_list_marker_and_content` reports for a task item.
+    let consumed = TASK_CHECKBOXES
+        .iter()
+        .find_map(|checkbox| after_padding.strip_prefix(checkbox).map(|_| 
checkbox.len()))
+        .unwrap_or(0);
+
+    let marker_end = line.len() - after_padding.len() + consumed;
+    let text = line[..marker_end].to_string();
+    Some(SourceMarker {
+        content_col: display_width(&text),
+        text,
+    })
+}
+
 pub(crate) fn extract_list_marker_and_content(line: &str) -> (String, String) {
     // First, find the leading indentation
     let indent_len = line.len() - line.trim_start().len();
@@ -120,20 +205,27 @@
 
     // Handle bullet lists
     // Trim trailing whitespace while preserving hard breaks
-    for bullet in ["- ", "* ", "+ "] {
-        if let Some(rest) = trimmed.strip_prefix(bullet) {
-            let marker_prefix = &bullet[..bullet.len() - 1]; // "-", "*", or 
"+"
-            // Include GFM task list checkboxes in the non-wrappable marker 
prefix
-            for checkbox in ["[ ] ", "[x] ", "[X] "] {
-                if let Some(content) = rest.strip_prefix(checkbox) {
-                    return (
-                        format!("{indent}{marker_prefix} {checkbox}"),
-                        trim_preserving_hard_break(content),
-                    );
-                }
-            }
-            return (format!("{indent}{bullet}"), 
trim_preserving_hard_break(rest));
+    for bullet in ['-', '*', '+'] {
+        let Some(after_bullet) = trimmed.strip_prefix(bullet) else {
+            continue;
+        };
+        let mut padding = after_bullet.chars();
+        if !padding.next().is_some_and(|c| MARKER_PADDING.contains(&c)) {
+            continue;
         }
+        // Include GFM task list checkboxes in the non-wrappable marker prefix
+        if let Some((checkbox, content)) = strip_task_checkbox(after_bullet) {
+            return (
+                format!("{indent}{bullet} {checkbox}"),
+                trim_preserving_hard_break(content),
+            );
+        }
+        // Only the first padding character belongs to the marker. Any further
+        // padding stays with the content, where reflow normalizes it away.
+        return (
+            format!("{indent}{bullet} "),
+            trim_preserving_hard_break(padding.as_str()),
+        );
     }
 
     // Handle numbered lists on trimmed content
@@ -143,20 +235,20 @@
     while let Some(c) = chars.next() {
         marker_content.push(c);
         if c == '.' {
-            // Check if next char is a space
+            // Check if next char is marker padding
             if let Some(next) = chars.next()
-                && next == ' '
+                && MARKER_PADDING.contains(&next)
             {
-                marker_content.push(next);
+                // Normalize the padding: a tab would otherwise land in the 
marker,
+                // where its byte length misreports the content column.
+                marker_content.push(' ');
                 let rest = chars.as_str();
                 // Check for GFM task list checkboxes
-                for checkbox in ["[ ] ", "[x] ", "[X] "] {
-                    if let Some(content) = rest.strip_prefix(checkbox) {
-                        return (
-                            format!("{indent}{marker_content}{checkbox}"),
-                            trim_preserving_hard_break(content),
-                        );
-                    }
+                if let Some((checkbox, content)) = strip_task_checkbox(rest) {
+                    return (
+                        format!("{indent}{marker_content}{checkbox}"),
+                        trim_preserving_hard_break(content),
+                    );
                 }
                 let content = trim_preserving_hard_break(rest);
                 return (format!("{indent}{marker_content}"), content);
@@ -187,9 +279,9 @@
     // Can have more digits
     while let Some(c) = chars.next() {
         if c == '.' {
-            // After period, must have a space (consistent with 
extract_list_marker_and_content)
+            // After period, must have marker padding (consistent with 
extract_list_marker_and_content)
             // "2019." alone is NOT treated as a list item to avoid false 
positives
-            return chars.next() == Some(' ');
+            return chars.next().is_some_and(|c| MARKER_PADDING.contains(&c));
         }
         if !c.is_numeric() {
             return false;
@@ -201,8 +293,7 @@
 pub(crate) fn is_list_item(line: &str) -> bool {
     // Bullet lists
     if (line.starts_with('-') || line.starts_with('*') || 
line.starts_with('+'))
-        && line.len() > 1
-        && line.chars().nth(1) == Some(' ')
+        && line.chars().nth(1).is_some_and(|c| MARKER_PADDING.contains(&c))
     {
         return true;
     }
@@ -540,6 +631,73 @@
         );
     }
 
+    #[test]
+    fn test_extract_list_marker_task_checkbox_with_wide_marker_spacing() {
+        // Extra spaces between the marker and the checkbox are marker padding.
+        // The checkbox must still be recognized, otherwise it is handed to the
+        // reflow engine as prose and "[ ]" collapses to "[]", silently turning
+        // a task item into plain text.
+        assert_eq!(
+            extract_list_marker_and_content("-   [ ] wide unchecked"),
+            ("- [ ] ".to_string(), "wide unchecked".to_string())
+        );
+        assert_eq!(
+            extract_list_marker_and_content("*  [x] wide checked"),
+            ("* [x] ".to_string(), "wide checked".to_string())
+        );
+        assert_eq!(
+            extract_list_marker_and_content("+    [X] wide checked upper"),
+            ("+ [X] ".to_string(), "wide checked upper".to_string())
+        );
+        assert_eq!(
+            extract_list_marker_and_content("  -  [ ] indented and wide"),
+            ("  - [ ] ".to_string(), "indented and wide".to_string())
+        );
+        // Ordered markers pad after the period.
+        assert_eq!(
+            extract_list_marker_and_content("1.   [ ] wide ordered"),
+            ("1. [ ] ".to_string(), "wide ordered".to_string())
+        );
+        assert_eq!(
+            extract_list_marker_and_content("99.  [x] wide multi-digit"),
+            ("99. [x] ".to_string(), "wide multi-digit".to_string())
+        );
+        // A tab is padding too: CommonMark expands it, rumdl's parser accepts
+        // it, and leaving it in place mangles the checkbox as extra spaces 
did.
+        assert_eq!(
+            extract_list_marker_and_content("- \t[ ] tab padded"),
+            ("- [ ] ".to_string(), "tab padded".to_string())
+        );
+        assert_eq!(
+            extract_list_marker_and_content("-\t[ ] tab only"),
+            ("- [ ] ".to_string(), "tab only".to_string())
+        );
+        assert_eq!(
+            extract_list_marker_and_content("1.\t[x] ordered tab only"),
+            ("1. [x] ".to_string(), "ordered tab only".to_string())
+        );
+        // A tab-padded item without a checkbox normalizes the marker too.
+        assert_eq!(
+            extract_list_marker_and_content("-\tplain tab item"),
+            ("- ".to_string(), "plain tab item".to_string())
+        );
+        assert_eq!(
+            extract_list_marker_and_content("1.\tplain ordered tab item"),
+            ("1. ".to_string(), "plain ordered tab item".to_string())
+        );
+        // Padding without a checkbox keeps the existing shape: the marker is
+        // normalized to one space and the padding stays with the content.
+        assert_eq!(
+            extract_list_marker_and_content("-   plain wide item"),
+            ("- ".to_string(), "  plain wide item".to_string())
+        );
+        // A bracket pair that is not a checkbox is content, not a marker.
+        assert_eq!(
+            extract_list_marker_and_content("-   [link] text"),
+            ("- ".to_string(), "  [link] text".to_string())
+        );
+    }
+
     #[test]
     fn test_is_horizontal_rule_commonmark_indent() {
         // Up to 3 spaces of leading indent is allowed (CommonMark thematic 
break).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/src/rules/md013_line_length/tests.rs 
new/rumdl-0.2.30/src/rules/md013_line_length/tests.rs
--- old/rumdl-0.2.28/src/rules/md013_line_length/tests.rs       2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/src/rules/md013_line_length/tests.rs       2026-07-09 
19:39:55.000000000 +0200
@@ -4302,6 +4302,30 @@
     );
 }
 
+/// A thematic break inside a blockquote is not a bullet list, whichever
+/// whitespace separates its markers. Reflowing it as list prose destroys it.
+#[test]
+fn test_blockquote_thematic_break_is_not_reflowed() {
+    let config = MD013Config {
+        line_length: crate::types::LineLength::new(40),
+        reflow: true,
+        reflow_mode: ReflowMode::Normalize,
+        ..Default::default()
+    };
+    let rule = MD013LineLength::from_config_struct(config);
+
+    for separator in [" ", "\t"] {
+        let body = std::iter::repeat_n("-", 
41).collect::<Vec<_>>().join(separator);
+        let content = format!("> {body}\n");
+        let ctx = LintContext::new(&content, MarkdownFlavor::Standard, None);
+        assert_eq!(
+            rule.fix(&ctx).unwrap(),
+            content,
+            "thematic break separated by {separator:?} was rewritten"
+        );
+    }
+}
+
 /// A GFM task-list checkbox item in a blockquote wraps with continuation lines
 /// aligned under the content (the checkbox is part of the marker, not the 
body).
 #[test]
@@ -4762,6 +4786,56 @@
     }
 
     #[test]
+    fn test_task_item_wide_marker_spacing_does_not_corrupt_checkbox() {
+        // Padding between the list marker and the checkbox is marker spacing,
+        // not content. Reflow must still recognize the task item; handing
+        // "[ ]" to the reflow engine as prose collapses it to "[]" and the
+        // item silently stops being a task item.
+        let rule = make_rule(80);
+        let tail = "This task has a really long description that exceeds the 
line limit and should wrap";
+        for marker in ["-  ", "-   ", "*  ", "+   ", "1.  ", "10.   ", "- \t", 
"-\t", "1.\t"] {
+            let content = format!("{marker}[ ] {tail}\n");
+            let ctx = LintContext::new(&content, MarkdownFlavor::Standard, 
None);
+            let fixed = rule.fix(&ctx).unwrap();
+            assert!(
+                !fixed.contains("[]"),
+                "marker {marker:?} corrupted the checkbox: {fixed:?}"
+            );
+            assert!(
+                fixed.contains("[ ] "),
+                "marker {marker:?} dropped the checkbox: {fixed:?}"
+            );
+        }
+    }
+
+    #[test]
+    fn test_tab_padded_item_stays_a_list_item() {
+        // rumdl's parser accepts a tab as marker padding. When MD013's own
+        // heuristic did not, the item was reflowed as a plain paragraph and 
its
+        // continuation landed at column 0, dissolving the list.
+        let rule = make_rule(40);
+        let ctx = LintContext::new(
+            "-\tAlpha beta gamma delta epsilon zeta eta theta\n",
+            MarkdownFlavor::Standard,
+            None,
+        );
+        let fixed = rule.fix(&ctx).unwrap();
+        assert_eq!(fixed, "- Alpha beta gamma delta epsilon zeta\n  eta 
theta\n");
+    }
+
+    #[test]
+    fn test_task_item_wide_marker_spacing_wraps_at_content_column() {
+        // The rebuilt marker is normalized to MD030's single space, so the
+        // continuation aligns under the checkbox content column, exactly as it
+        // does for a single-space source marker.
+        let rule = make_rule(40);
+        let content = "-   [ ] Alpha beta gamma delta epsilon zeta eta 
theta\n";
+        let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
+        let fixed = rule.fix(&ctx).unwrap();
+        assert_eq!(fixed, "- [ ] Alpha beta gamma delta epsilon\n      zeta 
eta theta\n");
+    }
+
+    #[test]
     fn test_task_item_all_bullet_markers() {
         // All bullet markers (-, *, +) should handle task checkboxes correctly
         let rule = make_rule(80);
@@ -9165,3 +9239,44 @@
     assert!(result[0].fix.is_some(), "First warning should have a fix");
     assert!(result[1].fix.is_some(), "Second warning should have a fix");
 }
+
+#[test]
+fn test_text_reflow_preserves_nested_code_blocks_in_lists() {
+    let config = MD013Config {
+        line_length: crate::types::LineLength::from_const(40),
+        reflow: true,
+        ..Default::default()
+    };
+    let mut rule = MD013LineLength::from_config_struct(config);
+    rule.list_spacing = MD030Config {
+        ul_single: crate::types::PositiveUsize::from_const(3),
+        ul_multi: crate::types::PositiveUsize::from_const(3),
+        ..Default::default()
+    };
+
+    let content = indoc! {"
+        -   This is a very long list item text that will definitely exceed the 
forty character limit and force a reflow.
+
+            ```
+            code block line 1
+            code block line 2
+            ```
+    "};
+
+    let ctx = LintContext::new(content, 
crate::config::MarkdownFlavor::Standard, None);
+    let fixed = rule.fix(&ctx).unwrap();
+
+    let expected = indoc! {"
+        -   This is a very long list item text
+            that will definitely exceed the
+            forty character limit and force a
+            reflow.
+
+            ```
+            code block line 1
+            code block line 2
+            ```
+    "};
+
+    assert_eq!(fixed, expected);
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/src/rules/md013_line_length.rs 
new/rumdl-0.2.30/src/rules/md013_line_length.rs
--- old/rumdl-0.2.28/src/rules/md013_line_length.rs     2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/src/rules/md013_line_length.rs     2026-07-09 
19:39:55.000000000 +0200
@@ -26,7 +26,7 @@
 use block_builder::{Block, BlockBuilder};
 use helpers::{
     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, is_unwrappable_line, 
split_into_segments,
+    is_list_item, is_standalone_link_or_image_line, is_unwrappable_line, 
source_list_marker, split_into_segments,
     trim_preserving_hard_break,
 };
 pub use md013_config::MD013Config;
@@ -1033,6 +1033,13 @@
             start_bq.prefix.clone()
         };
 
+        // A thematic break opens with what looks like a bullet marker (`- - 
-`).
+        // It is not a list item, and reflowing it as prose destroys the break.
+        // The top-level reflow path applies the same exemption.
+        if is_horizontal_rule(&start_bq.content) {
+            return (None, start_idx + 1);
+        }
+
         let (marker, first_body) = 
extract_list_marker_and_content(&start_bq.content);
         if marker.is_empty() {
             return (None, start_idx + 1);
@@ -1277,11 +1284,11 @@
         // content from column 2 to 4), its nested list/blockquote children 
are reflowed
         // independently and would otherwise keep their original indent — 
leaving them
         // under the parent's new content column, where a CommonMark parser 
reparses them
-        // as siblings rather than children. Each frame is (source content 
column,
+        // as siblings rather than children. Each frame is (normalized marker 
width,
         // cumulative shift applied to that item's content column); a 
descendant adds its
         // innermost open ancestor's shift to its own indent so the whole 
subtree moves
-        // together. With default MD030 every shift is 0, so this is inert and 
the output
-        // is byte-identical.
+        // together. With a default MD030 and no marker padding every shift is 
0, so this
+        // is inert and the output is byte-identical.
         let mut list_shift_stack: Vec<(usize, isize)> = Vec::new();
 
         let mut i = 0;
@@ -1970,6 +1977,12 @@
                 let list_start = i;
                 let (marker, first_content) = 
extract_list_marker_and_content(lines[i]);
                 let marker_len = marker.len();
+                // The normalized marker above is what gets re-emitted; the 
source marker
+                // is where the item's content actually starts. Nested blocks 
move by the
+                // difference between the two content columns, so the shift 
must be
+                // measured against the source, not against the normalized 
width.
+                let source_marker = source_list_marker(lines[i]);
+                let source_content_col = 
source_marker.as_ref().map_or(marker_len, |m| m.content_col);
 
                 // Checkbox ([ ]/[x]/[X]) is inline content, not part of the 
list marker.
                 // Use the base bullet/number marker width for continuation 
recognition
@@ -2421,9 +2434,14 @@
                         let spaces = 
self.list_spacing.expected_spaces(li.is_ordered, is_multi, bullet_len);
                         let new_marker = 
format!("{indent_prefix}{}{}{checkbox_tail}", li.marker, " ".repeat(spaces));
                         let new_col = new_marker.chars().count();
-                        let shift = new_col as isize - marker_len as isize;
+                        let shift = new_col as isize - source_content_col as 
isize;
                         (new_marker, new_col, shift)
                     } else {
+                        // MkDocs enforces a rigid structural indent, so the 
item is emitted
+                        // exactly as written and its nested blocks never 
move. Re-emitting
+                        // the normalized marker here would narrow the content 
column while
+                        // leaving those blocks behind.
+                        let marker = source_marker.map_or(marker, |m| m.text);
                         (marker, indent_size, 0isize)
                     };
                 let expected_indent = " ".repeat(indent_size);
@@ -2496,8 +2514,11 @@
                 // Record this item's frame so its nested children inherit the 
shift.
                 // Only a reflowed item's marker actually moves; an unreflowed 
one keeps
                 // its source position and so contributes no shift to its 
children. The
-                // source content column (`marker_len`) is the threshold that 
decides
-                // which following lines are inside this item.
+                // threshold that decides which following lines are inside 
this item is
+                // the normalized marker width, NOT `source_content_col`: the 
collection
+                // loop above gathers continuations by that same width, so the 
frame
+                // boundary must match it or the two would disagree about 
ownership of
+                // lines indented between the normalized and the source 
content column.
                 list_shift_stack.push((marker_len, if needs_reflow { 
code_indent_shift } else { 0 }));
 
                 if needs_reflow {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/src/rules/md032_blanks_around_lists.rs 
new/rumdl-0.2.30/src/rules/md032_blanks_around_lists.rs
--- old/rumdl-0.2.28/src/rules/md032_blanks_around_lists.rs     2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/src/rules/md032_blanks_around_lists.rs     2026-07-09 
19:39:55.000000000 +0200
@@ -1009,6 +1009,61 @@
         rule.fix(&ctx).expect("Lint fix failed")
     }
 
+    #[test]
+    fn test_fix_does_not_split_item_before_different_list_type() {
+        // The continuation line belongs to the bullet item. Only one blank 
line
+        // is needed, between the two lists; inserting one after the marker 
line
+        // would split the item into a list plus a stray paragraph.
+        let content = "- alpha beta\n  aligned\n1. ordered item\n   cont\n";
+        assert_eq!(fix(content), "- alpha beta\n  aligned\n\n1. ordered item\n 
  cont\n");
+
+        // check() anchors the boundary on the first list's last line and the
+        // second list's first line, exactly as it does when neither item 
wraps.
+        let warnings = lint(content);
+        assert_eq!(warnings.len(), 2);
+        assert_eq!(warnings[0].line, 2);
+        assert_eq!(warnings[1].line, 3);
+    }
+
+    #[test]
+    fn test_fix_does_not_split_blockquoted_item_before_different_list_type() {
+        let content = "> - alpha beta\n>   aligned\n> 1. ordered item\n";
+        assert_eq!(fix(content), "> - alpha beta\n>   aligned\n>\n> 1. ordered 
item\n");
+    }
+
+    #[test]
+    fn test_fix_keeps_lazy_continuation_with_its_item() {
+        // Per CommonMark the lazy line continues the item's paragraph, so the
+        // blank belongs after it, before the new list. Splitting it off would
+        // promote it to a standalone paragraph and change the rendering.
+        let content = "- alpha beta\nlazy\n1. ordered item\n";
+        assert_eq!(fix(content), "- alpha beta\nlazy\n\n1. ordered item\n");
+
+        let warnings = lint(content);
+        assert_eq!(warnings.len(), 2);
+        assert_eq!(warnings[0].line, 2);
+        assert_eq!(warnings[1].line, 3);
+    }
+
+    #[test]
+    fn test_fix_keeps_blockquoted_lazy_continuation_with_its_item() {
+        let content = "> - alpha beta\n> lazy\n> 1. ordered item\n";
+        assert_eq!(fix(content), "> - alpha beta\n> lazy\n>\n> 1. ordered 
item\n");
+    }
+
+    #[test]
+    fn test_fix_indents_lazy_continuation_when_not_allowed() {
+        // With allow_lazy_continuation = false the lazy line is first indented
+        // into the item (Phase 0), then the blank separates the two lists.
+        let rule = MD032BlanksAroundLists::from_config_struct(MD032Config {
+            allow_lazy_continuation: false,
+        });
+        let content = "- alpha beta\nlazy\n1. ordered item\n";
+        let ctx = LintContext::new(content, 
crate::config::MarkdownFlavor::Standard, None);
+        let fixed = rule.fix(&ctx).expect("Lint fix failed");
+        assert_eq!(fixed, "- alpha beta\n  lazy\n\n1. ordered item\n");
+    }
+
     // Test that warnings include Fix objects
     fn check_warnings_have_fixes(content: &str) {
         let warnings = lint(content);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rumdl-0.2.28/tests/formats/md013_mkdocs_reflow_test.rs 
new/rumdl-0.2.30/tests/formats/md013_mkdocs_reflow_test.rs
--- old/rumdl-0.2.28/tests/formats/md013_mkdocs_reflow_test.rs  2026-07-03 
11:03:19.000000000 +0200
+++ new/rumdl-0.2.30/tests/formats/md013_mkdocs_reflow_test.rs  2026-07-09 
19:39:55.000000000 +0200
@@ -1332,7 +1332,7 @@
     let expected = "\
 # Test
 
-1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
+1.  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
     tempor incididunt ut labore et dolore magna aliqua.
 
     !!! note
@@ -1372,7 +1372,7 @@
     let fixed = rule.fix(&ctx).unwrap();
 
     let expected = "\
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
+-   Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
     tempor incididunt ut labore et dolore magna aliqua.
 
     !!! note
@@ -1413,7 +1413,7 @@
     let fixed = rule.fix(&ctx).unwrap();
 
     let expected = "\
-1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
+1.  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
     tempor incididunt ut labore et dolore magna aliqua.
 
     !!! note
@@ -1496,7 +1496,7 @@
     // MD030 normalizes "1.  " to "1. " — only the overlong line and
     // marker spacing should change, everything else verbatim.
     let expected = "\
-1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
+1.  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
     tempor incididunt ut labore et dolore magna aliqua.
 
     !!! note
@@ -1546,7 +1546,7 @@
     let fixed = rule.fix(&ctx).unwrap();
 
     let expected = "\
-1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
+1.  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
     tempor incididunt ut labore et dolore magna aliqua.
 
     !!! note
@@ -1595,7 +1595,7 @@
     let fixed = rule.fix(&ctx).unwrap();
 
     let expected = "\
-1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
+1.  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
     tempor incididunt ut labore et dolore magna aliqua.
 
     !!! note
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rumdl-0.2.28/tests/regressions/lint_context_list_blocks_type_change_test.rs 
new/rumdl-0.2.30/tests/regressions/lint_context_list_blocks_type_change_test.rs
--- 
old/rumdl-0.2.28/tests/regressions/lint_context_list_blocks_type_change_test.rs 
    1970-01-01 01:00:00.000000000 +0100
+++ 
new/rumdl-0.2.30/tests/regressions/lint_context_list_blocks_type_change_test.rs 
    2026-07-09 19:39:55.000000000 +0200
@@ -0,0 +1,87 @@
+// Regression tests: a list block that ends because a list of a *different 
type*
+// follows must keep its continuation lines, both properly indented ones and
+// lazy (column 0) ones. Per CommonMark a lazy line continues the item's
+// paragraph, so it belongs to the block.
+//
+// When the block was truncated to its last marker line, MD032 saw the item as
+// ending at the marker and inserted a blank line *inside* the item, splitting
+// it into a list plus a stray paragraph.
+
+use rumdl_lib::config::MarkdownFlavor;
+use rumdl_lib::lint_context::LintContext;
+
+fn blocks(content: &str) -> Vec<(usize, usize)> {
+    let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
+    ctx.list_blocks.iter().map(|b| (b.start_line, b.end_line)).collect()
+}
+
+#[test]
+fn test_bullet_continuation_kept_when_ordered_list_follows() {
+    // Line 2 is indented to the bullet's content column, so it belongs to 
item 1.
+    assert_eq!(
+        blocks("- alpha beta\n  aligned\n1. ordered item\n"),
+        vec![(1, 2), (3, 3)]
+    );
+}
+
+#[test]
+fn test_ordered_continuation_kept_when_bullet_list_follows() {
+    assert_eq!(
+        blocks("1. alpha beta\n   aligned\n- bullet item\n"),
+        vec![(1, 2), (3, 3)]
+    );
+}
+
+#[test]
+fn test_multiple_continuation_lines_kept() {
+    assert_eq!(
+        blocks("- alpha beta\n  second\n  third\n1. ordered item\n"),
+        vec![(1, 3), (4, 4)]
+    );
+}
+
+#[test]
+fn test_lazy_continuation_kept_when_different_type_follows() {
+    // A column-0 line after a list item lazily continues the item's paragraph,
+    // so the block owns it. Ending the block at the marker line instead made
+    // MD032 insert its blank there, splitting the paragraph in two.
+    assert_eq!(blocks("- alpha beta\nlazy\n1. ordered item\n"), vec![(1, 2), 
(3, 3)]);
+}
+
+#[test]
+fn test_multiple_lazy_continuations_kept() {
+    assert_eq!(
+        blocks("- alpha beta\nlazy one\nlazy two\n1. ordered item\n"),
+        vec![(1, 3), (4, 4)]
+    );
+}
+
+#[test]
+fn test_same_type_list_still_merges_continuations() {
+    // A same-type marker never triggers the trim; the whole run is one block.
+    assert_eq!(blocks("- alpha beta\n  aligned\n- second item\n"), vec![(1, 
3)]);
+}
+
+#[test]
+fn test_no_continuation_is_unaffected() {
+    assert_eq!(blocks("- alpha beta\n1. ordered item\n"), vec![(1, 1), (2, 
2)]);
+}
+
+#[test]
+fn test_blockquote_continuation_kept_when_different_type_follows() {
+    // Inside a blockquote the raw indent is 0 because of the `>` marker. The
+    // indentation that matters is the one after the blockquote prefix.
+    assert_eq!(
+        blocks("> - alpha beta\n>   aligned\n> 1. ordered item\n"),
+        vec![(1, 2), (3, 3)]
+    );
+}
+
+#[test]
+fn test_blockquote_lazy_continuation_kept() {
+    // No indentation after the `>`, so this line is lazy, as at the root 
level.
+    assert_eq!(
+        blocks("> - alpha beta\n> lazy\n> 1. ordered item\n"),
+        vec![(1, 2), (3, 3)]
+    );
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.28/tests/regressions/mod.rs 
new/rumdl-0.2.30/tests/regressions/mod.rs
--- old/rumdl-0.2.28/tests/regressions/mod.rs   2026-07-03 11:03:19.000000000 
+0200
+++ new/rumdl-0.2.30/tests/regressions/mod.rs   2026-07-09 19:39:55.000000000 
+0200
@@ -6,6 +6,7 @@
 mod final_confidence_assessment;
 mod html_comments_test;
 mod lint_context_list_blocks_issue_148_test;
+mod lint_context_list_blocks_type_change_test;
 mod lint_context_list_continuation_test;
 mod lint_context_visual_indent_test;
 mod md009_md013_integration_test;

++++++ rumdl.obsinfo ++++++
--- /var/tmp/diff_new_pack.ygU0rn/_old  2026-07-10 17:45:33.413926547 +0200
+++ /var/tmp/diff_new_pack.ygU0rn/_new  2026-07-10 17:45:33.437927369 +0200
@@ -1,5 +1,5 @@
 name: rumdl
-version: 0.2.28
-mtime: 1783069399
-commit: c5f50cbbbc790d7adb5583b51e4322ed3dcae65c
+version: 0.2.30
+mtime: 1783618795
+commit: 12e34feb01c07d62a8161b5ac74940ffe8954c21
 

++++++ vendor.tar.zst ++++++
/work/SRC/openSUSE:Factory/rumdl/vendor.tar.zst 
/work/SRC/openSUSE:Factory/.rumdl.new.1991/vendor.tar.zst differ: char 7, line 1

Reply via email to