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-29 19:00:40
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/rumdl (Old)
 and      /work/SRC/openSUSE:Factory/.rumdl.new.2004 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "rumdl"

Wed Jul 29 19:00:40 2026 rev:93 rq:1368296 version:0.2.45

Changes:
--------
--- /work/SRC/openSUSE:Factory/rumdl/rumdl.changes      2026-07-28 
17:59:24.227615336 +0200
+++ /work/SRC/openSUSE:Factory/.rumdl.new.2004/rumdl.changes    2026-07-29 
19:02:24.788190977 +0200
@@ -1,0 +2,19 @@
+Wed Jul 29 06:00:19 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 0.2.45:
+  * Fixed
+    - md013: keep every word when a sentence boundary falls inside
+      emphasis (207823b)
+    - md037: don't flag text between emphasis spans when a span
+      wraps (17db4f9)
+  * Changed
+    - md013: reflow output changed for emphasis spanning several
+      sentences. In sentence-per-line and semantic-line-breaks
+      modes, **First. Second.** now becomes **First.\nSecond.**
+      instead of **First.**\n**Second.**. Reopening the markers on
+      each line replaced the sentence-separating space with a
+      marker pair, which deleted the space and, on many inputs,
+      whole words. A line break inside a span is whitespace in
+      CommonMark, so the rendered output is unchanged.
+
+-------------------------------------------------------------------

Old:
----
  rumdl-0.2.44.obscpio

New:
----
  rumdl-0.2.45.obscpio

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

Other differences:
------------------
++++++ rumdl.spec ++++++
--- /var/tmp/diff_new_pack.rCFS8M/_old  2026-07-29 19:02:28.888332282 +0200
+++ /var/tmp/diff_new_pack.rCFS8M/_new  2026-07-29 19:02:28.888332282 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           rumdl
-Version:        0.2.44
+Version:        0.2.45
 Release:        0
 Summary:        Markdown Linter written in Rust
 License:        MIT

++++++ _service ++++++
--- /var/tmp/diff_new_pack.rCFS8M/_old  2026-07-29 19:02:28.952334488 +0200
+++ /var/tmp/diff_new_pack.rCFS8M/_new  2026-07-29 19:02:28.956334626 +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.44</param>
+    <param name="revision">refs/tags/v0.2.45</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.rCFS8M/_old  2026-07-29 19:02:28.988335729 +0200
+++ /var/tmp/diff_new_pack.rCFS8M/_new  2026-07-29 19:02:28.996336004 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param name="url">https://github.com/rvben/rumdl.git</param>
-              <param 
name="changesrevision">f4ec7f95f17043a4ddfeedeafef49a9f698e3845</param></service></servicedata>
+              <param 
name="changesrevision">0610735e0d36bc1cfd5a77cc720c08132c241182</param></service></servicedata>
 (No newline at EOF)
 

++++++ rumdl-0.2.44.obscpio -> rumdl-0.2.45.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.44/CHANGELOG.md 
new/rumdl-0.2.45/CHANGELOG.md
--- old/rumdl-0.2.44/CHANGELOG.md       2026-07-28 11:12:36.000000000 +0200
+++ new/rumdl-0.2.45/CHANGELOG.md       2026-07-28 20:41:14.000000000 +0200
@@ -84,6 +84,23 @@
 
 
 
+
+## [0.2.45](https://github.com/rvben/rumdl/compare/v0.2.44...v0.2.45) - 
2026-07-28
+
+### Fixed
+
+- **md013**: keep every word when a sentence boundary falls inside emphasis 
([207823b](https://github.com/rvben/rumdl/commit/207823b664ea5ca34c2c7c1ec5b2335366ea5b17))
+- **md037**: don't flag text between emphasis spans when a span wraps 
([17db4f9](https://github.com/rvben/rumdl/commit/17db4f9437bd7f63fee5f552712cd6503caa2d48))
+
+### Changed
+
+- **md013**: reflow output changed for emphasis spanning several sentences. In
+  `sentence-per-line` and `semantic-line-breaks` modes, `**First. Second.**` 
now
+  becomes `**First.\nSecond.**` instead of `**First.**\n**Second.**`. 
Reopening the
+  markers on each line replaced the sentence-separating space with a marker 
pair,
+  which deleted the space and, on many inputs, whole words. A line break 
inside a
+  span is whitespace in CommonMark, so the rendered output is unchanged.
+
 ## [0.2.44](https://github.com/rvben/rumdl/compare/v0.2.43...v0.2.44) - 
2026-07-28
 
 ### Added
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.44/Cargo.lock new/rumdl-0.2.45/Cargo.lock
--- old/rumdl-0.2.44/Cargo.lock 2026-07-28 11:12:36.000000000 +0200
+++ new/rumdl-0.2.45/Cargo.lock 2026-07-28 20:41:14.000000000 +0200
@@ -2289,7 +2289,7 @@
 
 [[package]]
 name = "rumdl"
-version = "0.2.44"
+version = "0.2.45"
 dependencies = [
  "anyhow",
  "assert_cmd",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.44/Cargo.toml new/rumdl-0.2.45/Cargo.toml
--- old/rumdl-0.2.44/Cargo.toml 2026-07-28 11:12:36.000000000 +0200
+++ new/rumdl-0.2.45/Cargo.toml 2026-07-28 20:41:14.000000000 +0200
@@ -1,6 +1,6 @@
 [package]
 name = "rumdl"
-version = "0.2.44"
+version = "0.2.45"
 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.44/README.md new/rumdl-0.2.45/README.md
--- old/rumdl-0.2.44/README.md  2026-07-28 11:12:36.000000000 +0200
+++ new/rumdl-0.2.45/README.md  2026-07-28 20:41:14.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.44
+    rev: v0.2.45
     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.44
+    rev: v0.2.45
     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.44
+    rev: v0.2.45
     hooks:
       - id: rumdl
         args: [--no-exclude]  # Disable all exclude patterns
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.44/docs/getting-started/quickstart.md 
new/rumdl-0.2.45/docs/getting-started/quickstart.md
--- old/rumdl-0.2.44/docs/getting-started/quickstart.md 2026-07-28 
11:12:36.000000000 +0200
+++ new/rumdl-0.2.45/docs/getting-started/quickstart.md 2026-07-28 
20:41:14.000000000 +0200
@@ -96,7 +96,7 @@
 ```yaml
 repos:
   - repo: https://github.com/rvben/rumdl-pre-commit
-    rev: v0.2.44  # Use latest version
+    rev: v0.2.45  # Use latest version
     hooks:
       - id: rumdl
 ```
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.44/docs/global-settings.md 
new/rumdl-0.2.45/docs/global-settings.md
--- old/rumdl-0.2.44/docs/global-settings.md    2026-07-28 11:12:36.000000000 
+0200
+++ new/rumdl-0.2.45/docs/global-settings.md    2026-07-28 20:41:14.000000000 
+0200
@@ -1513,7 +1513,7 @@
 
 ```yaml
 - repo: https://github.com/rvben/rumdl-pre-commit
-  rev: v0.2.44
+  rev: v0.2.45
   hooks:
     - id: rumdl
       args: [--config=.rumdl.toml]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.44/docs/mdformat-comparison.md 
new/rumdl-0.2.45/docs/mdformat-comparison.md
--- old/rumdl-0.2.44/docs/mdformat-comparison.md        2026-07-28 
11:12:36.000000000 +0200
+++ new/rumdl-0.2.45/docs/mdformat-comparison.md        2026-07-28 
20:41:14.000000000 +0200
@@ -232,7 +232,7 @@
 
    # After
    - repo: https://github.com/rvben/rumdl-pre-commit
-     rev: v0.2.44
+     rev: v0.2.45
      hooks:
        - id: rumdl
     ```
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.44/docs/usage/ci-cd.md 
new/rumdl-0.2.45/docs/usage/ci-cd.md
--- old/rumdl-0.2.44/docs/usage/ci-cd.md        2026-07-28 11:12:36.000000000 
+0200
+++ new/rumdl-0.2.45/docs/usage/ci-cd.md        2026-07-28 20:41:14.000000000 
+0200
@@ -40,7 +40,7 @@
 ```yaml
 - uses: rvben/rumdl@v0
   with:
-    version: "0.2.44"
+    version: "0.2.45"
     path: docs/
 ```
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.44/docs/usage/pre-commit.md 
new/rumdl-0.2.45/docs/usage/pre-commit.md
--- old/rumdl-0.2.44/docs/usage/pre-commit.md   2026-07-28 11:12:36.000000000 
+0200
+++ new/rumdl-0.2.45/docs/usage/pre-commit.md   2026-07-28 20:41:14.000000000 
+0200
@@ -13,7 +13,7 @@
 ```yaml title=".pre-commit-config.yaml"
 repos:
   - repo: https://github.com/rvben/rumdl-pre-commit
-    rev: v0.2.44  # Use latest version
+    rev: v0.2.45  # 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.44/npm/cli-darwin-arm64/package.json 
new/rumdl-0.2.45/npm/cli-darwin-arm64/package.json
--- old/rumdl-0.2.44/npm/cli-darwin-arm64/package.json  2026-07-28 
11:12:36.000000000 +0200
+++ new/rumdl-0.2.45/npm/cli-darwin-arm64/package.json  2026-07-28 
20:41:14.000000000 +0200
@@ -1,6 +1,6 @@
 {
   "name": "@rumdl/cli-darwin-arm64",
-  "version": "0.2.44",
+  "version": "0.2.45",
   "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.44/npm/cli-darwin-x64/package.json 
new/rumdl-0.2.45/npm/cli-darwin-x64/package.json
--- old/rumdl-0.2.44/npm/cli-darwin-x64/package.json    2026-07-28 
11:12:36.000000000 +0200
+++ new/rumdl-0.2.45/npm/cli-darwin-x64/package.json    2026-07-28 
20:41:14.000000000 +0200
@@ -1,6 +1,6 @@
 {
   "name": "@rumdl/cli-darwin-x64",
-  "version": "0.2.44",
+  "version": "0.2.45",
   "description": "rumdl binary for macOS x64 (Intel)",
   "license": "MIT",
   "repository": {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.44/npm/cli-linux-arm64/package.json 
new/rumdl-0.2.45/npm/cli-linux-arm64/package.json
--- old/rumdl-0.2.44/npm/cli-linux-arm64/package.json   2026-07-28 
11:12:36.000000000 +0200
+++ new/rumdl-0.2.45/npm/cli-linux-arm64/package.json   2026-07-28 
20:41:14.000000000 +0200
@@ -1,6 +1,6 @@
 {
   "name": "@rumdl/cli-linux-arm64",
-  "version": "0.2.44",
+  "version": "0.2.45",
   "description": "rumdl binary for Linux ARM64 (glibc)",
   "license": "MIT",
   "repository": {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.44/npm/cli-linux-arm64-musl/package.json 
new/rumdl-0.2.45/npm/cli-linux-arm64-musl/package.json
--- old/rumdl-0.2.44/npm/cli-linux-arm64-musl/package.json      2026-07-28 
11:12:36.000000000 +0200
+++ new/rumdl-0.2.45/npm/cli-linux-arm64-musl/package.json      2026-07-28 
20:41:14.000000000 +0200
@@ -1,6 +1,6 @@
 {
   "name": "@rumdl/cli-linux-arm64-musl",
-  "version": "0.2.44",
+  "version": "0.2.45",
   "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.44/npm/cli-linux-x64/package.json 
new/rumdl-0.2.45/npm/cli-linux-x64/package.json
--- old/rumdl-0.2.44/npm/cli-linux-x64/package.json     2026-07-28 
11:12:36.000000000 +0200
+++ new/rumdl-0.2.45/npm/cli-linux-x64/package.json     2026-07-28 
20:41:14.000000000 +0200
@@ -1,6 +1,6 @@
 {
   "name": "@rumdl/cli-linux-x64",
-  "version": "0.2.44",
+  "version": "0.2.45",
   "description": "rumdl binary for Linux x64 (glibc)",
   "license": "MIT",
   "repository": {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.44/npm/cli-linux-x64-musl/package.json 
new/rumdl-0.2.45/npm/cli-linux-x64-musl/package.json
--- old/rumdl-0.2.44/npm/cli-linux-x64-musl/package.json        2026-07-28 
11:12:36.000000000 +0200
+++ new/rumdl-0.2.45/npm/cli-linux-x64-musl/package.json        2026-07-28 
20:41:14.000000000 +0200
@@ -1,6 +1,6 @@
 {
   "name": "@rumdl/cli-linux-x64-musl",
-  "version": "0.2.44",
+  "version": "0.2.45",
   "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.44/npm/cli-win32-x64/package.json 
new/rumdl-0.2.45/npm/cli-win32-x64/package.json
--- old/rumdl-0.2.44/npm/cli-win32-x64/package.json     2026-07-28 
11:12:36.000000000 +0200
+++ new/rumdl-0.2.45/npm/cli-win32-x64/package.json     2026-07-28 
20:41:14.000000000 +0200
@@ -1,6 +1,6 @@
 {
   "name": "@rumdl/cli-win32-x64",
-  "version": "0.2.44",
+  "version": "0.2.45",
   "description": "rumdl binary for Windows x64",
   "license": "MIT",
   "repository": {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.44/npm/rumdl/package.json 
new/rumdl-0.2.45/npm/rumdl/package.json
--- old/rumdl-0.2.44/npm/rumdl/package.json     2026-07-28 11:12:36.000000000 
+0200
+++ new/rumdl-0.2.45/npm/rumdl/package.json     2026-07-28 20:41:14.000000000 
+0200
@@ -1,6 +1,6 @@
 {
   "name": "rumdl",
-  "version": "0.2.44",
+  "version": "0.2.45",
   "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.44",
-    "@rumdl/cli-darwin-arm64": "0.2.44",
-    "@rumdl/cli-linux-x64": "0.2.44",
-    "@rumdl/cli-linux-arm64": "0.2.44",
-    "@rumdl/cli-linux-x64-musl": "0.2.44",
-    "@rumdl/cli-linux-arm64-musl": "0.2.44",
-    "@rumdl/cli-win32-x64": "0.2.44"
+    "@rumdl/cli-darwin-x64": "0.2.45",
+    "@rumdl/cli-darwin-arm64": "0.2.45",
+    "@rumdl/cli-linux-x64": "0.2.45",
+    "@rumdl/cli-linux-arm64": "0.2.45",
+    "@rumdl/cli-linux-x64-musl": "0.2.45",
+    "@rumdl/cli-linux-arm64-musl": "0.2.45",
+    "@rumdl/cli-win32-x64": "0.2.45"
   }
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rumdl-0.2.44/src/rules/md037_spaces_around_emphasis.rs 
new/rumdl-0.2.45/src/rules/md037_spaces_around_emphasis.rs
--- old/rumdl-0.2.44/src/rules/md037_spaces_around_emphasis.rs  2026-07-28 
11:12:36.000000000 +0200
+++ new/rumdl-0.2.45/src/rules/md037_spaces_around_emphasis.rs  2026-07-28 
20:41:14.000000000 +0200
@@ -125,6 +125,40 @@
         // Check reference definitions [ref]: url "title" using pre-computed 
data (O(1) vs O(n))
         ctx.is_in_reference_def(byte_pos)
     }
+
+    /// Where each emphasis span ends, ascending, paired with where it began.
+    ///
+    /// Ordering by end position is what keeps 
[`Self::closes_earlier_emphasis`]
+    /// logarithmic: a run only has to consider the spans that close inside it,
+    /// which is a handful, rather than every span in the document.
+    fn emphasis_span_ends(ctx: &crate::lint_context::LintContext) -> 
Vec<(usize, usize)> {
+        let mut ends: Vec<(usize, usize)> = ctx
+            .emphasis_spans()
+            .iter()
+            .map(|span| (span.byte_end, span.byte_offset))
+            .collect();
+        ends.sort_unstable();
+        ends
+    }
+
+    /// Check whether a flagged run only looks spaced because its opening 
marker
+    /// is really the closing delimiter of emphasis that began further up.
+    ///
+    /// Markers are paired one line at a time, so on a line that continues a 
span
+    /// the first marker reads as an opener and every marker after it pairs one
+    /// position off, putting ordinary words between what look like delimiters.
+    /// The document-level parse settles it: a marker already spent closing a
+    /// span cannot also open one.
+    ///
+    /// A span that both starts and ends inside the run is a different thing, 
and
+    /// stays reported: `* _real_ *` really is emphasis with spaces in it.
+    fn closes_earlier_emphasis(span_ends: &[(usize, usize)], start: usize, 
end: usize) -> bool {
+        let first_after_start = span_ends.partition_point(|(span_end, _)| 
*span_end <= start);
+        span_ends[first_after_start..]
+            .iter()
+            .take_while(|(span_end, _)| *span_end < end)
+            .any(|(_, span_start)| *span_start <= start)
+    }
 }
 
 impl Rule for MD037NoSpaceInEmphasis {
@@ -197,6 +231,11 @@
         // Filter out warnings for emphasis markers that are inside links, 
HTML comments, math, or MkDocs markup
         let mut filtered_warnings = Vec::new();
         let lines = ctx.raw_lines();
+        let span_ends = if warnings.is_empty() {
+            Vec::new()
+        } else {
+            Self::emphasis_span_ends(ctx)
+        };
 
         for (line_idx, line) in lines.iter().enumerate() {
             let line_num = line_idx + 1;
@@ -221,7 +260,9 @@
                     // detector grammar, so MD037's spaced-emphasis warnings 
can
                     // never land inside one.
                     let in_pandoc_construct = 
ctx.flavor.is_pandoc_compatible() && ctx.is_in_bracketed_span(byte_pos);
+                    let byte_end = line_start_pos + (warning.end_column - 1);
                     if !in_pandoc_construct
+                        && !Self::closes_earlier_emphasis(&span_ends, 
byte_pos, byte_end)
                         && !self.is_in_link(ctx, byte_pos)
                         && !ctx.is_in_html_comment(byte_pos)
                         && !is_in_math_context(ctx, byte_pos)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.44/src/utils/text_reflow.rs 
new/rumdl-0.2.45/src/utils/text_reflow.rs
--- old/rumdl-0.2.44/src/utils/text_reflow.rs   2026-07-28 11:12:36.000000000 
+0200
+++ new/rumdl-0.2.45/src/utils/text_reflow.rs   2026-07-28 20:41:14.000000000 
+0200
@@ -841,7 +841,71 @@
     )
 }
 
+/// Reflow a line, falling back to the input when the result would not 
preserve it.
+///
+/// Reflow redistributes whitespace: it decides where lines break, never which
+/// characters a paragraph contains. So the sequence of non-whitespace 
characters
+/// is invariant across a correct reflow, and any difference means the reflow
+/// dropped, duplicated, reordered, or invented content. Returning the input
+/// unchanged in that case costs a paragraph that stays unwrapped; the 
alternative
+/// is writing corrupted prose into the user's file. A caller comparing its
+/// replacement against the original then sees no change and reports nothing.
 pub fn reflow_line(line: &str, options: &ReflowOptions) -> Vec<String> {
+    let reflowed = reflow_line_unchecked(line, options);
+    if preserves_content(line, &reflowed) {
+        reflowed
+    } else {
+        vec![line.to_string()]
+    }
+}
+
+/// Whether `reflowed` still holds `original`'s text: the same non-whitespace
+/// characters in the same order, with every word boundary intact.
+///
+/// Reflow may add a boundary the input did not have, since wrapping a script
+/// that writes without spaces has to break somewhere. Removing one is 
different:
+/// it glues two words into a word the author never wrote.
+fn preserves_content(original: &str, reflowed: &[String]) -> bool {
+    let (original_text, original_breaks) = 
visible_text_and_breaks(original.chars());
+    let (reflowed_text, reflowed_breaks) =
+        visible_text_and_breaks(reflowed.iter().flat_map(|line| 
line.chars().chain(['\n'])));
+
+    original_text == reflowed_text && contains_all(&reflowed_breaks, 
&original_breaks)
+}
+
+/// The non-whitespace characters of `text`, and for each interior run of
+/// whitespace, how many characters precede it.
+fn visible_text_and_breaks(text: impl Iterator<Item = char>) -> (String, 
Vec<usize>) {
+    let mut visible = String::new();
+    let mut breaks = Vec::new();
+    let mut count = 0usize;
+    let mut pending_break = false;
+
+    for c in text {
+        if c.is_whitespace() {
+            pending_break = count > 0;
+        } else {
+            if pending_break {
+                breaks.push(count);
+                pending_break = false;
+            }
+            visible.push(c);
+            count += 1;
+        }
+    }
+
+    (visible, breaks)
+}
+
+/// Whether every value in `subset` appears in `superset`. Both are ascending.
+fn contains_all(superset: &[usize], subset: &[usize]) -> bool {
+    let mut candidates = superset.iter();
+    subset
+        .iter()
+        .all(|wanted| candidates.by_ref().any(|found| found == wanted))
+}
+
+fn reflow_line_unchecked(line: &str, options: &ReflowOptions) -> Vec<String> {
     // For sentence-per-line mode, always process regardless of length
     if options.sentence_per_line {
         let elements = parse_elements(line, options);
@@ -1998,47 +2062,59 @@
     let mut current_line = String::new();
 
     for (idx, element) in elements.iter().enumerate() {
-        // For text elements, split into sentences
-        if let Element::Text(text) = element {
-            // Simply append text - it already has correct spacing from 
tokenization
-            let combined = format!("{current_line}{text}");
+        // Text and emphasis are absorbed the same way. An emphasis span is
+        // rendered back to its source form and then treated as ordinary text,
+        // so a sentence boundary inside it breaks the line without closing and
+        // reopening the markers: a line break inside a span is whitespace, and
+        // whitespace is all a reflow is allowed to change.
+        let piece = match element {
+            // Text already carries its own spacing from tokenization.
+            Element::Text(text) => Some(text.clone()),
+            Element::Italic { content, underscore } => Some(wrap_emphasis(
+                content,
+                if *underscore { "_" } else { "*" },
+                &mut current_line,
+            )),
+            Element::Bold { content, underscore } => Some(wrap_emphasis(
+                content,
+                if *underscore { "__" } else { "**" },
+                &mut current_line,
+            )),
+            Element::Strikethrough { content, double } => Some(wrap_emphasis(
+                content,
+                if *double { "~~" } else { "~" },
+                &mut current_line,
+            )),
+            _ => None,
+        };
+
+        if let Some(piece) = piece {
+            let combined = format!("{current_line}{piece}");
             // Use the pre-computed abbreviations set to avoid redundant 
computation
             let sentences = split_into_sentences_with_set(&combined, 
&abbreviations, require_sentence_capital);
 
             if sentences.len() > 1 {
-                // We found sentence boundaries
+                // Accumulate rather than emit-and-overwrite: a sentence held
+                // back for the next element must absorb what follows it, or 
the
+                // text that follows would reach the output ahead of it.
+                let mut pending = String::new();
+                let last = sentences.len() - 1;
                 for (i, sentence) in sentences.iter().enumerate() {
-                    if i == 0 {
-                        // First sentence might continue from previous elements
-                        // But check if it ends with an abbreviation
-                        let trimmed = sentence.trim();
-
-                        if text_ends_with_abbreviation(trimmed, 
&abbreviations) {
-                            // Don't emit yet - this sentence ends with 
abbreviation, continue accumulating
-                            current_line.clone_from(sentence);
-                        } else {
-                            // Normal case - emit the first sentence
-                            lines.push(sentence.clone());
-                            current_line.clear();
-                        }
-                    } else if i == sentences.len() - 1 {
-                        // Last sentence: check if it's complete or incomplete
-                        let trimmed = sentence.trim();
-                        let ends_with_sentence_punct = 
ends_with_sentence_punct(trimmed);
-
-                        if ends_with_sentence_punct && 
!text_ends_with_abbreviation(trimmed, &abbreviations) {
-                            // Complete sentence - emit it immediately
-                            lines.push(sentence.clone());
-                            current_line.clear();
-                        } else {
-                            // Incomplete sentence - save for next iteration
-                            current_line.clone_from(sentence);
-                        }
-                    } else {
-                        // Complete sentences in the middle
-                        lines.push(sentence.clone());
+                    if !pending.is_empty() {
+                        pending.push(' ');
+                    }
+                    pending.push_str(sentence);
+
+                    // The splitter already decided every boundary except the
+                    // final one, which is just the leftover tail. Hold a tail
+                    // that no punctuation closed, and hold any piece ending in
+                    // an abbreviation the splitter broke after regardless.
+                    let closed = i < last || 
ends_with_sentence_punct(&pending);
+                    if closed && !text_ends_with_abbreviation(&pending, 
&abbreviations) {
+                        lines.push(std::mem::take(&mut pending));
                     }
                 }
+                current_line = pending;
             } else {
                 // Single sentence - check if it's complete
                 let trimmed = combined.trim();
@@ -2062,38 +2138,6 @@
                     current_line = combined;
                 }
             }
-        } else if let Element::Italic { content, underscore } = element {
-            // Handle italic elements - may contain multiple sentences that 
need continuation
-            let marker = if *underscore { "_" } else { "*" };
-            handle_emphasis_sentence_split(
-                content,
-                marker,
-                &abbreviations,
-                require_sentence_capital,
-                &mut current_line,
-                &mut lines,
-            );
-        } else if let Element::Bold { content, underscore } = element {
-            // Handle bold elements - may contain multiple sentences that need 
continuation
-            let marker = if *underscore { "__" } else { "**" };
-            handle_emphasis_sentence_split(
-                content,
-                marker,
-                &abbreviations,
-                require_sentence_capital,
-                &mut current_line,
-                &mut lines,
-            );
-        } else if let Element::Strikethrough { content, double } = element {
-            // Handle strikethrough elements - may contain multiple sentences 
that need continuation
-            handle_emphasis_sentence_split(
-                content,
-                if *double { "~~" } else { "~" },
-                &abbreviations,
-                require_sentence_capital,
-                &mut current_line,
-                &mut lines,
-            );
         } else {
             // Non-text, non-emphasis elements (Code, Links, etc.)
             let element_str = format!("{element}");
@@ -2124,82 +2168,14 @@
     lines
 }
 
-/// Handle splitting emphasis content at sentence boundaries while preserving 
markers
-fn handle_emphasis_sentence_split(
-    content: &str,
-    marker: &str,
-    abbreviations: &HashSet<String>,
-    require_sentence_capital: bool,
-    current_line: &mut String,
-    lines: &mut Vec<String>,
-) {
-    // Split the emphasis content into sentences
-    let sentences = split_into_sentences_with_set(content, abbreviations, 
require_sentence_capital);
-
-    if sentences.len() <= 1 {
-        // Single sentence or no boundaries - treat as atomic
-        if should_insert_space_before_join(current_line) {
-            current_line.push(' ');
-        }
-        current_line.push_str(marker);
-        current_line.push_str(content);
-        current_line.push_str(marker);
-
-        // Check if the emphasis content ends with sentence punctuation - if 
so, emit
-        let trimmed = content.trim();
-        let ends_with_punct = ends_with_sentence_punct(trimmed);
-        if ends_with_punct && !text_ends_with_abbreviation(trimmed, 
abbreviations) {
-            lines.push(current_line.clone());
-            current_line.clear();
-        }
-    } else {
-        // Multiple sentences - each gets its own emphasis markers
-        for (i, sentence) in sentences.iter().enumerate() {
-            let trimmed = sentence.trim();
-            if trimmed.is_empty() {
-                continue;
-            }
-
-            if i == 0 {
-                // First sentence: combine with current_line and emit
-                if should_insert_space_before_join(current_line) {
-                    current_line.push(' ');
-                }
-                current_line.push_str(marker);
-                current_line.push_str(trimmed);
-                current_line.push_str(marker);
-
-                // Check if this is a complete sentence
-                let ends_with_punct = ends_with_sentence_punct(trimmed);
-                if ends_with_punct && !text_ends_with_abbreviation(trimmed, 
abbreviations) {
-                    lines.push(current_line.clone());
-                    current_line.clear();
-                }
-            } else if i == sentences.len() - 1 {
-                // Last sentence: check if complete
-                let ends_with_punct = ends_with_sentence_punct(trimmed);
-
-                let mut line = String::new();
-                line.push_str(marker);
-                line.push_str(trimmed);
-                line.push_str(marker);
-
-                if ends_with_punct && !text_ends_with_abbreviation(trimmed, 
abbreviations) {
-                    lines.push(line);
-                } else {
-                    // Incomplete - keep in current_line for potential 
continuation
-                    *current_line = line;
-                }
-            } else {
-                // Middle sentences: emit with markers
-                let mut line = String::new();
-                line.push_str(marker);
-                line.push_str(trimmed);
-                line.push_str(marker);
-                lines.push(line);
-            }
-        }
+/// Restore an emphasis span to its source form, opening a gap before it when
+/// the line it joins needs one. Unlike text elements, a span carries no
+/// surrounding whitespace of its own.
+fn wrap_emphasis(content: &str, marker: &str, current_line: &mut String) -> 
String {
+    if should_insert_space_before_join(current_line) {
+        current_line.push(' ');
     }
+    format!("{marker}{content}{marker}")
 }
 
 /// English break-words used for semantic line break splitting.
@@ -4121,6 +4097,66 @@
 mod tests {
     use super::*;
 
+    /// `preserves_content` is the last line of defense against a reflow 
writing
+    /// corrupted prose into a file, so it has to actually reject the ways a
+    /// reflow can go wrong - not merely accept the ways it can go right.
+    #[test]
+    fn preserves_content_accepts_whitespace_changes_and_rejects_the_rest() {
+        let accepted: &[(&str, &[&str])] = &[
+            ("one two three", &["one two three"]),
+            ("one two three", &["one two", "three"]),
+            ("one two three", &["one", "two", "three"]),
+            // Collapsing runs of whitespace and dropping trailing whitespace
+            ("one   two  ", &["one two"]),
+            // A script written without spaces has to break somewhere
+            ("日本語のテキスト", &["日本語の", "テキスト"]),
+            // Markers move to the line their content moved to
+            ("_First. Second._", &["_First.", "Second._"]),
+        ];
+        for (original, reflowed) in accepted {
+            let reflowed: Vec<String> = 
reflowed.iter().map(ToString::to_string).collect();
+            assert!(
+                preserves_content(original, &reflowed),
+                "{original:?} -> {reflowed:?} only moves whitespace"
+            );
+        }
+
+        let rejected: &[(&str, &[&str])] = &[
+            // Dropped
+            ("one two three", &["one two"]),
+            // Invented
+            ("one two", &["one two three"]),
+            // Reordered
+            ("one two", &["two one"]),
+            // Duplicated
+            ("_First. Second._", &["_First._", "_Second._"]),
+            // Two words glued into one
+            ("alpha and beta", &["alpha", "andbeta"]),
+            // A space deleted around punctuation
+            ("mot suivant : autre", &["mot suivant: autre"]),
+        ];
+        for (original, reflowed) in rejected {
+            let reflowed: Vec<String> = 
reflowed.iter().map(ToString::to_string).collect();
+            assert!(
+                !preserves_content(original, &reflowed),
+                "{original:?} -> {reflowed:?} changes the text, not just its 
line breaks"
+            );
+        }
+    }
+
+    /// A rejected reflow leaves the line alone rather than writing the damage.
+    #[test]
+    fn reflow_line_falls_back_to_the_input_when_content_would_change() {
+        let options = ReflowOptions {
+            line_length: 40,
+            ..Default::default()
+        };
+        let line = "one two three four five six seven eight nine ten";
+
+        assert!(preserves_content(line, &reflow_line(line, &options)));
+        assert_eq!(reflow_line(line, &options), reflow_line_unchecked(line, 
&options));
+    }
+
     #[test]
     fn cascade_split_line_handles_a_very_long_line_without_overflowing() {
         // A single line of thousands of words once drove `cascade_split_line`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.44/tests/formats/sentence_per_line_test.rs 
new/rumdl-0.2.45/tests/formats/sentence_per_line_test.rs
--- old/rumdl-0.2.44/tests/formats/sentence_per_line_test.rs    2026-07-28 
11:12:36.000000000 +0200
+++ new/rumdl-0.2.45/tests/formats/sentence_per_line_test.rs    2026-07-28 
20:41:14.000000000 +0200
@@ -864,22 +864,12 @@
 
 #[test]
 fn test_emphasis_wrapping_multiple_sentences() {
-    // Test case from issue #360 - emphasis spanning multiple sentences
+    // Emphasis spanning multiple sentences
     let rule = create_sentence_per_line_rule();
     let content = "**First sentence. Second sentence.**\n";
     let ctx = LintContext::new(content, 
rumdl_lib::config::MarkdownFlavor::Standard, None);
     let result = rule.check(&ctx).unwrap();
 
-    println!("Content: {content:?}");
-    println!("Warnings: {:?}", result.len());
-    for w in &result {
-        println!("  Warning: {}", w.message);
-        if let Some(fix) = &w.fix {
-            println!("    Fix range: {:?}", fix.range);
-            println!("    Replacement: {:?}", fix.replacement);
-        }
-    }
-
     assert!(!result.is_empty(), "Should detect multiple sentences");
     assert!(result[0].fix.is_some(), "Should have fix");
 
@@ -887,12 +877,14 @@
 
     // The fix should NOT have leading spaces on the second line
     assert!(
-        !fix.replacement.contains("  **Second"),
+        !fix.replacement.contains("  Second"),
         "Second sentence should not have leading spaces: {:?}",
         fix.replacement
     );
 
-    // The expected output is each sentence on its own line with emphasis 
preserved
-    let expected = "**First sentence.**\n**Second sentence.**\n";
+    // Each sentence gets its own line. The span is not reopened per line: a 
line
+    // break inside emphasis is whitespace, and duplicating the markers would
+    // delete the space the closing marker replaces.
+    let expected = "**First sentence.\nSecond sentence.**\n";
     assert_eq!(fix.replacement, expected, "Fix should produce correct output");
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rumdl-0.2.44/tests/regressions/md013_reflow_emphasis_content_loss_issue_767_test.rs
 
new/rumdl-0.2.45/tests/regressions/md013_reflow_emphasis_content_loss_issue_767_test.rs
--- 
old/rumdl-0.2.44/tests/regressions/md013_reflow_emphasis_content_loss_issue_767_test.rs
     1970-01-01 01:00:00.000000000 +0100
+++ 
new/rumdl-0.2.45/tests/regressions/md013_reflow_emphasis_content_loss_issue_767_test.rs
     2026-07-28 20:41:14.000000000 +0200
@@ -0,0 +1,213 @@
+//! Regression tests for issues #767 and #768: MD013 reflow deleted words when 
a
+//! sentence boundary fell inside an emphasis span.
+//!
+//! Reflow reopened the span on each new line, and the marker it wrote consumed
+//! the space that separated the sentences, so the surrounding words were
+//! swallowed. A quoted, emphasized paragraph could lose half its text.
+//!
+//! Every test runs the real `rumdl fmt` pipeline. The core assertion is the
+//! invariant reflow must never break: it chooses where lines end, so the 
output
+//! holds exactly the input's non-whitespace characters, in order.
+
+use std::fs;
+use std::path::Path;
+use tempfile::TempDir;
+
+const SENTENCE_PER_LINE: &[&str] = &[
+    "MD013.reflow = true",
+    "MD013.reflow-mode = \"sentence-per-line\"",
+    "MD013.line-length = 0",
+];
+
+const SEMANTIC_LINE_BREAKS: &[&str] = &[
+    "MD013.reflow = true",
+    "MD013.reflow-mode = \"semantic-line-breaks\"",
+    "MD013.line-length = 80",
+];
+
+/// Run `rumdl fmt` on `content` and return the formatted file.
+fn fmt(dir: &Path, name: &str, settings: &[&str], content: &str) -> String {
+    let file_path = dir.join(name);
+    fs::write(&file_path, content).unwrap();
+
+    let mut command = std::process::Command::new(env!("CARGO_BIN_EXE_rumdl"));
+    command.arg("fmt").arg("--no-config").arg("--no-cache");
+    for setting in settings {
+        command.arg("-c").arg(setting);
+    }
+
+    let output = command.arg(&file_path).output().expect("Failed to execute 
rumdl");
+    let status = output.status.code();
+    assert!(
+        status == Some(0) || status == Some(1),
+        "rumdl fmt should succeed, got status {status:?}; stderr: {}",
+        String::from_utf8_lossy(&output.stderr)
+    );
+
+    fs::read_to_string(&file_path).unwrap()
+}
+
+/// The document's content: every non-whitespace character except the 
blockquote
+/// markers, which are structure. Splitting a quoted paragraph legitimately 
adds
+/// a `>` prefix to each new line.
+fn content_of(markdown: &str) -> String {
+    markdown
+        .lines()
+        .map(|line| line.trim_start().trim_start_matches(['>', ' ', '\t']))
+        .flat_map(|line| line.chars())
+        .filter(|c| !c.is_whitespace())
+        .collect()
+}
+
+/// Format `input` twice, asserting reflow changed only whitespace and 
converged.
+fn assert_reflow_preserves(name: &str, settings: &[&str], input: &str) -> 
String {
+    let temp = TempDir::new().unwrap();
+    let once = fmt(temp.path(), "a.md", settings, input);
+    let twice = fmt(temp.path(), "b.md", settings, &once);
+
+    assert_eq!(
+        content_of(input),
+        content_of(&once),
+        "{name}: reflow changed content, not just line 
breaks.\ninput:\n{input}\ngot:\n{once}"
+    );
+    assert_eq!(
+        once, twice,
+        "{name}: reflow must be idempotent.\nfirst:\n{once}\nsecond:\n{twice}"
+    );
+    once
+}
+
+/// The reporter's table from #767: an emphasized, quoted paragraph in every
+/// marker style and every container. Each one lost words before the fix.
+#[test]
+fn test_emphasis_spanning_sentences_keeps_every_word() {
+    let cases: &[(&str, &str)] = &[
+        (
+            "plain",
+            r#""The reviewers called it "a solid contribution." The paper was 
accepted.""#,
+        ),
+        (
+            "italic",
+            "_The paper was accepted without revision. Publication follows in 
March._",
+        ),
+        (
+            "italic_parens",
+            "_The paper was accepted (after one revision.) Publication follows 
in March._",
+        ),
+        (
+            "italic_code",
+            "_The paper was accepted `after one revision.` Publication follows 
in March._",
+        ),
+        (
+            "italic_quote",
+            r#"_The reviewers called it "a solid contribution." The paper was 
accepted._"#,
+        ),
+        (
+            "italic_single_quote",
+            "_The reviewers called it 'a solid contribution.' The paper was 
accepted._",
+        ),
+        (
+            "italic_leading_quote",
+            r#"_"A solid contribution." The paper was accepted without 
revision._"#,
+        ),
+        (
+            "italic_nested_quotes",
+            r#"_"The reviewers called it "a solid contribution." The paper was 
accepted."_"#,
+        ),
+        (
+            "bold_nested_quotes",
+            r#"**"The reviewers called it "a solid contribution." The paper 
was accepted."**"#,
+        ),
+        (
+            "three_sentences",
+            r#"_The reviewers called it "a solid contribution." The paper was 
accepted. Publication follows in March._"#,
+        ),
+        (
+            "two_spans",
+            r#"_"She said "yes." The vote carried."_ and _"He said "no." The 
motion failed."_"#,
+        ),
+        (
+            "list_item",
+            r#"- _"The reviewers called it "a solid contribution." The paper 
was accepted."_"#,
+        ),
+        (
+            "nested_list",
+            "- outer\n  - _\"The reviewers called it \"a solid contribution.\" 
The paper was accepted.\"_",
+        ),
+        (
+            "blockquote",
+            r#"> _"The reviewers called it "a solid contribution." The paper 
was accepted."_"#,
+        ),
+        (
+            "strikethrough",
+            r#"~~"The reviewers called it "a solid contribution." The paper 
was accepted."~~"#,
+        ),
+        (
+            "underscore_bold",
+            r#"__"The reviewers called it "a solid contribution." The paper 
was accepted."__"#,
+        ),
+        (
+            "asterisk_italic",
+            r#"*"The reviewers called it "a solid contribution." The paper was 
accepted."*"#,
+        ),
+    ];
+
+    for (name, paragraph) in cases {
+        let input = format!("# Notes\n\n{paragraph}\n");
+        assert_reflow_preserves(name, SEMANTIC_LINE_BREAKS, &input);
+    }
+}
+
+/// The worst case from #767: two emphasized spans on one line, each with 
nested
+/// quotes. Reflow used to reduce this to a fragment of its words.
+#[test]
+fn test_two_emphasis_spans_on_one_line() {
+    let input = "# Notes\n\n_\"She said \"yes.\" The vote carried.\"_ and 
_\"He said \"no.\" The motion failed.\"_\n";
+    let fixed = assert_reflow_preserves("two_spans", SEMANTIC_LINE_BREAKS, 
input);
+
+    for word in ["She said", "The vote carried", "He said", "The motion 
failed"] {
+        assert!(fixed.contains(word), "'{word}' must survive reflow. 
got:\n{fixed}");
+    }
+}
+
+/// #768: a bold span inside a list item, followed by two plain sentences. The
+/// trailing sentence disappeared entirely.
+#[test]
+fn test_bold_span_in_list_item_keeps_trailing_sentences() {
+    let input = "- **A \"Widget vs. Gadget: which do I pick?\" guide** - 
decision-oriented. \
+                 Heuristic: pick Widget for one-off jobs, Gadget for repeat 
jobs. \
+                 This sentence must survive.\n";
+    let fixed = assert_reflow_preserves("list_bold_quote", SENTENCE_PER_LINE, 
input);
+
+    assert!(
+        fixed.contains("This sentence must survive."),
+        "the trailing sentence must survive reflow. got:\n{fixed}"
+    );
+}
+
+/// #768: a bold span spanning four sentences inside a blockquote. Each 
sentence
+/// gets its own quoted line, and the span still opens and closes exactly once.
+#[test]
+fn test_bold_span_in_blockquote_splits_without_reopening() {
+    let input = "> **Alpha. Beta. Gamma. Delta.**\n";
+    let fixed = assert_reflow_preserves("blockquote_bold", SENTENCE_PER_LINE, 
input);
+
+    assert_eq!(fixed, "> **Alpha.\n> Beta.\n> Gamma.\n> Delta.**\n");
+    assert_eq!(fixed.matches("**").count(), 2, "the span must not be reopened 
per line");
+}
+
+/// #768: reflow must not indent the lines it creates.
+#[test]
+fn test_split_lines_are_not_indented() {
+    let input = "**Choosing a type.**\n\
+                 Ask one question: *does this affect what users receive?*\n\
+                 Yes selects one type; no selects the other.\n";
+    let fixed = assert_reflow_preserves("leading_spaces", SENTENCE_PER_LINE, 
input);
+
+    for line in fixed.lines() {
+        assert!(
+            !line.starts_with(' '),
+            "reflow must not indent a line it created. got:\n{fixed}"
+        );
+    }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rumdl-0.2.44/tests/regressions/md037_multiline_span_false_positive_test.rs 
new/rumdl-0.2.45/tests/regressions/md037_multiline_span_false_positive_test.rs
--- 
old/rumdl-0.2.44/tests/regressions/md037_multiline_span_false_positive_test.rs  
    1970-01-01 01:00:00.000000000 +0100
+++ 
new/rumdl-0.2.45/tests/regressions/md037_multiline_span_false_positive_test.rs  
    2026-07-28 20:41:14.000000000 +0200
@@ -0,0 +1,98 @@
+//! Regression tests for MD037 on emphasis that spans more than one line.
+//!
+//! Markers are paired one line at a time. On a line that continues a span, the
+//! first marker on that line is really the span's *closing* delimiter, but the
+//! line-local pairing read it as an opener, so every marker after it paired 
one
+//! position off. Ordinary words then sat between what looked like delimiters,
+//! MD037 reported "spaces inside emphasis markers", and `rumdl fmt` deleted
+//! those spaces: `_ and _` became `_and_`, joining two words into one.
+//!
+//! The document-level parse settles it: a marker already spent closing a span
+//! cannot also open one.
+
+use rumdl_lib::config::MarkdownFlavor;
+use rumdl_lib::lint_context::LintContext;
+use rumdl_lib::rule::Rule;
+use rumdl_lib::rules::MD037NoSpaceInEmphasis;
+
+fn check(content: &str) -> Vec<String> {
+    let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
+    MD037NoSpaceInEmphasis
+        .check(&ctx)
+        .unwrap()
+        .into_iter()
+        .map(|w| format!("{}:{} {}", w.line, w.column, w.message))
+        .collect()
+}
+
+fn fix(content: &str) -> String {
+    let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
+    MD037NoSpaceInEmphasis.fix(&ctx).unwrap()
+}
+
+/// The failing case: two quoted spans, the first continuing onto a second 
line.
+/// The `and` between the spans was reported as emphasized-with-spaces.
+#[test]
+fn test_text_between_spans_is_not_flagged_when_a_span_wraps() {
+    let content = "_\"She said \"yes.\"\nThe vote carried.\"_ and _\"He said 
\"no.\" The motion failed.\"_\n";
+
+    assert!(
+        check(content).is_empty(),
+        "text between two spans is not emphasis: {:?}",
+        check(content)
+    );
+    assert_eq!(fix(content), content, "fix must not touch the spaces around 
'and'");
+}
+
+/// Control: the same text on a single line was always correct, so the bug is
+/// specific to a span crossing a line boundary.
+#[test]
+fn test_single_line_control_is_unaffected() {
+    let content = "_\"She said \"yes.\" The vote carried.\"_ and _\"He said 
\"no.\" The motion failed.\"_\n";
+    assert!(check(content).is_empty(), "{:?}", check(content));
+}
+
+/// Control: a wrapped span without interior quotes was always correct.
+#[test]
+fn test_wrapped_span_without_quotes_control() {
+    let content = "_She said yes.\nThe vote carried._ and _She said no. The 
motion failed._\n";
+    assert!(check(content).is_empty(), "{:?}", check(content));
+}
+
+/// A genuine violation must still be reported, including on a line that also
+/// continues a real span. Suppressing the false positive must not blind the 
rule.
+#[test]
+fn test_real_violations_still_reported() {
+    let plain = "This is * spaced emphasis * text.\n";
+    assert_eq!(
+        check(plain).len(),
+        1,
+        "plain violation must be flagged: {:?}",
+        check(plain)
+    );
+
+    let after_wrapped_span = "_alpha\nbeta._ then * spaced * here\n";
+    let warnings = check(after_wrapped_span);
+    assert_eq!(
+        warnings.len(),
+        1,
+        "a violation after a wrapped span must still be flagged: {warnings:?}"
+    );
+    assert!(warnings[0].starts_with("2:"), "on the second line: {warnings:?}");
+    assert_eq!(fix(after_wrapped_span), "_alpha\nbeta._ then *spaced* here\n");
+}
+
+/// A span that both opens and closes inside the flagged run is a different
+/// shape from a span that only closes inside it, and must stay reported:
+/// `* _real_ *` really is emphasis with spaces in it.
+#[test]
+fn test_span_nested_inside_the_run_is_still_a_violation() {
+    let content = "Text * _real_ * more.\n";
+    assert_eq!(
+        check(content).len(),
+        1,
+        "spaces inside the outer markers are a violation: {:?}",
+        check(content)
+    );
+    assert_eq!(fix(content), "Text *_real_* more.\n");
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.44/tests/regressions/mod.rs 
new/rumdl-0.2.45/tests/regressions/mod.rs
--- old/rumdl-0.2.44/tests/regressions/mod.rs   2026-07-28 11:12:36.000000000 
+0200
+++ new/rumdl-0.2.45/tests/regressions/mod.rs   2026-07-28 20:41:14.000000000 
+0200
@@ -14,6 +14,7 @@
 mod md009_md013_order_test;
 mod md013_hard_breaks_test;
 mod md013_html_test;
+mod md013_reflow_emphasis_content_loss_issue_767_test;
 mod md013_reflow_french_punctuation_issue_740_test;
 mod md013_reflow_math_blocks_issue_762_test;
 mod md013_reflow_ordered_lookalike_issue_760_test;
@@ -23,6 +24,7 @@
 mod md033_edge_cases_test;
 mod md033_link_img_fix_test;
 mod md034_parentheses_url_test;
+mod md037_multiline_span_false_positive_test;
 mod md037_xxxx_regression_test;
 mod md038_false_positive_test;
 mod md040_blockquote_fence_issue_684_test;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rumdl-0.2.44/tests/utils/text_reflow_test.rs 
new/rumdl-0.2.45/tests/utils/text_reflow_test.rs
--- old/rumdl-0.2.44/tests/utils/text_reflow_test.rs    2026-07-28 
11:12:36.000000000 +0200
+++ new/rumdl-0.2.45/tests/utils/text_reflow_test.rs    2026-07-28 
20:41:14.000000000 +0200
@@ -1,6 +1,20 @@
 use rumdl_lib::utils::text_reflow::*;
 use std::time::Instant;
 
+/// Assert that reflow only moved line breaks: the joined output must hold the
+/// same non-whitespace characters, in the same order, as the input.
+///
+/// Reflow decides where a paragraph breaks, never which characters it 
contains,
+/// so any difference here is content loss or invented markup.
+fn assert_whitespace_only(input: &str, result: &[String]) {
+    let visible = |s: &str| s.chars().filter(|c| 
!c.is_whitespace()).collect::<String>();
+    assert_eq!(
+        visible(input),
+        visible(&result.join("\n")),
+        "reflow changed content, not just line breaks: {result:?}"
+    );
+}
+
 #[test]
 fn test_list_item_trailing_whitespace_removal() {
     // Test for issue #76 - hard breaks (2 trailing spaces) should be preserved
@@ -1843,9 +1857,12 @@
     }
 }
 
-/// Issue #251: Sentence reflow & formatting markers (bold, italic)
-/// When reflowing multi-sentence emphasized text, emphasis markers should
-/// continue across line breaks to maintain formatting on each line.
+/// Sentence reflow inside emphasis (bold, italic, strikethrough).
+///
+/// A multi-sentence emphasized span splits at its sentence boundaries like any
+/// other text. The span itself is left alone: a line break inside emphasis is
+/// whitespace, so the markers stay where the author put them and the rendered
+/// output is unchanged. Reflow moves line breaks and nothing else.
 mod issue_251_emphasis_continuation {
     use super::*;
 
@@ -1922,23 +1939,12 @@
         let input = "*Sentence one. Sentence two. Sentence three.*";
         let result = reflow_line(input, &options);
 
-        // Each sentence should have its own italic markers
-        assert_eq!(result.len(), 3, "Should have 3 lines: {result:?}");
-        assert!(
-            result[0].starts_with('*') && result[0].ends_with('*'),
-            "First line should have italic markers: {:?}",
-            result[0]
-        );
-        assert!(
-            result[1].starts_with('*') && result[1].ends_with('*'),
-            "Second line should have italic markers: {:?}",
-            result[1]
-        );
-        assert!(
-            result[2].starts_with('*') && result[2].ends_with('*'),
-            "Third line should have italic markers: {:?}",
-            result[2]
+        assert_eq!(
+            result,
+            vec!["*Sentence one.", "Sentence two.", "Sentence three.*"],
+            "Should split at sentence boundaries and leave the span intact"
         );
+        assert_whitespace_only(input, &result);
     }
 
     #[test]
@@ -1952,23 +1958,12 @@
         let input = "_Sentence one. Sentence two. Sentence three._";
         let result = reflow_line(input, &options);
 
-        // Each sentence should have its own italic markers (underscore style)
-        assert_eq!(result.len(), 3, "Should have 3 lines: {result:?}");
-        assert!(
-            result[0].starts_with('_') && result[0].ends_with('_'),
-            "First line should have underscore markers: {:?}",
-            result[0]
-        );
-        assert!(
-            result[1].starts_with('_') && result[1].ends_with('_'),
-            "Second line should have underscore markers: {:?}",
-            result[1]
-        );
-        assert!(
-            result[2].starts_with('_') && result[2].ends_with('_'),
-            "Third line should have underscore markers: {:?}",
-            result[2]
+        assert_eq!(
+            result,
+            vec!["_Sentence one.", "Sentence two.", "Sentence three._"],
+            "Should split at sentence boundaries and leave the span intact"
         );
+        assert_whitespace_only(input, &result);
     }
 
     #[test]
@@ -1982,18 +1977,12 @@
         let input = "**Sentence one. Sentence two.**";
         let result = reflow_line(input, &options);
 
-        // Each sentence should have its own bold markers
-        assert_eq!(result.len(), 2, "Should have 2 lines: {result:?}");
-        assert!(
-            result[0].starts_with("**") && result[0].ends_with("**"),
-            "First line should have bold markers: {:?}",
-            result[0]
-        );
-        assert!(
-            result[1].starts_with("**") && result[1].ends_with("**"),
-            "Second line should have bold markers: {:?}",
-            result[1]
+        assert_eq!(
+            result,
+            vec!["**Sentence one.", "Sentence two.**"],
+            "Should split at the sentence boundary and leave the span intact"
         );
+        assert_whitespace_only(input, &result);
     }
 
     #[test]
@@ -2007,18 +1996,12 @@
         let input = "__Sentence one. Sentence two.__";
         let result = reflow_line(input, &options);
 
-        // Each sentence should have its own bold markers (underscore style)
-        assert_eq!(result.len(), 2, "Should have 2 lines: {result:?}");
-        assert!(
-            result[0].starts_with("__") && result[0].ends_with("__"),
-            "First line should have underscore bold markers: {:?}",
-            result[0]
-        );
-        assert!(
-            result[1].starts_with("__") && result[1].ends_with("__"),
-            "Second line should have underscore bold markers: {:?}",
-            result[1]
+        assert_eq!(
+            result,
+            vec!["__Sentence one.", "Sentence two.__"],
+            "Should split at the sentence boundary and leave the span intact"
         );
+        assert_whitespace_only(input, &result);
     }
 
     // ============================================================
@@ -2033,41 +2016,22 @@
             ..Default::default()
         };
 
-        // The exact pattern from issue #251
+        // A quoted, emphasized citation spanning three sentences
         let input = r#"He said, _"There is this whole spectrum of crazy 
futures. But the one that I feel we're almost guaranteed to get. It's the same 
either way"_ [^ref]."#;
         let result = reflow_markdown(input, &options);
 
-        let lines: Vec<&str> = result.lines().collect();
+        let lines: Vec<String> = result.lines().map(str::to_string).collect();
 
-        // Should split into multiple sentences, each with emphasis markers
-        assert!(
-            lines.len() >= 3,
-            "Should have at least 3 lines for 3 sentences: {result:?}"
-        );
-
-        // First line should start with context and have opening emphasis
-        assert!(
-            lines[0].contains("_\"There is this whole spectrum"),
-            "First line should have opening quote with emphasis: {:?}",
-            lines[0]
-        );
-
-        // Middle lines should have emphasis markers on both ends
-        for line in &lines[1..lines.len() - 1] {
-            if !line.trim().is_empty() && !line.starts_with("He said") {
-                assert!(
-                    line.trim().starts_with('_') || line.contains("_\""),
-                    "Middle line should start with emphasis: {line:?}"
-                );
-            }
-        }
-
-        // Last line should have closing emphasis with quote and footnote
-        let last_line = lines.last().unwrap();
-        assert!(
-            last_line.contains("\"_") || last_line.ends_with('_'),
-            "Last line should have closing emphasis: {last_line:?}"
+        assert_eq!(
+            lines,
+            vec![
+                r#"He said, _"There is this whole spectrum of crazy futures."#,
+                "But the one that I feel we're almost guaranteed to get.",
+                r#"It's the same either way"_ [^ref]."#,
+            ],
+            "Each sentence gets its own line; the emphasis opens once and 
closes once"
         );
+        assert_whitespace_only(input, &lines);
     }
 
     #[test]
@@ -2078,25 +2042,16 @@
             ..Default::default()
         };
 
-        // Simplified version of issue #251
+        // A quoted, emphasized pair of sentences
         let input = r#"_"First sentence. Second sentence."_"#;
         let result = reflow_line(input, &options);
 
-        assert_eq!(result.len(), 2, "Should have 2 lines: {result:?}");
-
-        // First sentence should have opening quote inside emphasis
-        assert!(
-            result[0].starts_with("_\"") && result[0].ends_with('_'),
-            "First line: {:?}",
-            result[0]
-        );
-
-        // Second sentence should have closing quote inside emphasis
-        assert!(
-            result[1].starts_with('_') && result[1].ends_with("\"_"),
-            "Second line: {:?}",
-            result[1]
+        assert_eq!(
+            result,
+            vec![r#"_"First sentence."#, r#"Second sentence."_"#],
+            "The quote and the emphasis each open once and close once"
         );
+        assert_whitespace_only(input, &result);
     }
 
     // ============================================================
@@ -2168,19 +2123,12 @@
             ..Default::default()
         };
 
-        // Bold text containing sentences - each should get markers
+        // Bold text containing two sentences
         let input = "**First bold sentence. Second bold sentence.**";
         let result = reflow_line(input, &options);
 
-        assert_eq!(result.len(), 2, "Should have 2 lines: {result:?}");
-
-        // Each line should have bold markers
-        for (i, line) in result.iter().enumerate() {
-            assert!(
-                line.starts_with("**") && line.ends_with("**"),
-                "Line {i} should have bold markers: {line:?}"
-            );
-        }
+        assert_eq!(result, vec!["**First bold sentence.", "Second bold 
sentence.**"]);
+        assert_whitespace_only(input, &result);
     }
 
     #[test]
@@ -2239,16 +2187,14 @@
         let input = "*Sentence one. Sentence two.*";
         let result = reflow_line(input, &options);
 
-        // All markers should be asterisks, not underscores
+        // The author's asterisks survive verbatim; none are rewritten or added
+        assert_eq!(result, vec!["*Sentence one.", "Sentence two.*"]);
+        assert_whitespace_only(input, &result);
         for line in &result {
             assert!(
                 !line.contains('_'),
                 "Asterisk emphasis should not become underscore: {line:?}"
             );
-            assert!(
-                line.starts_with('*') && line.ends_with('*'),
-                "Should use asterisk markers: {line:?}"
-            );
         }
     }
 
@@ -2263,13 +2209,13 @@
         let input = "_Sentence one. Sentence two._";
         let result = reflow_line(input, &options);
 
-        // All markers should be underscores, not asterisks
+        // The author's underscores survive verbatim; none are rewritten or 
added
+        assert_eq!(result, vec!["_Sentence one.", "Sentence two._"]);
+        assert_whitespace_only(input, &result);
         for line in &result {
-            // Check that we don't have asterisks acting as emphasis markers
-            // (asterisks in content are OK, but the wrapper should be 
underscore)
             assert!(
-                line.starts_with('_') && line.ends_with('_'),
-                "Should use underscore markers: {line:?}"
+                !line.contains('*'),
+                "Underscore emphasis should not become asterisk: {line:?}"
             );
         }
     }
@@ -2691,16 +2637,15 @@
         let input = format!("*{long_sentence}*");
         let result = reflow_line(&input, &options);
 
-        // Should split into 50 sentences
+        // Should split into 50 sentences, with the single span still opening 
on
+        // the first line and closing on the last
         assert_eq!(result.len(), 50, "Should have 50 sentences");
-
-        // Each line should have emphasis markers
-        for line in &result {
-            assert!(
-                line.starts_with('*') && line.ends_with('*'),
-                "Each line should have emphasis: {line}"
-            );
+        assert_eq!(result[0], "*This is a sentence.");
+        assert_eq!(result[49], "This is a sentence.*");
+        for line in &result[1..49] {
+            assert_eq!(line, "This is a sentence.", "Interior lines carry no 
markers");
         }
+        assert_whitespace_only(&input, &result);
     }
 
     #[test]
@@ -3576,10 +3521,9 @@
     let input = "**First sentence. Second sentence.**";
     let result = reflow_line(input, &options);
 
-    // Should split into two lines, each with its own emphasis markers
-    assert_eq!(result.len(), 2, "Should produce 2 lines: {result:?}");
-    assert_eq!(result[0], "**First sentence.**");
-    assert_eq!(result[1], "**Second sentence.**");
+    // Should split at the sentence boundary, leaving the single span intact
+    assert_eq!(result, vec!["**First sentence.", "Second sentence.**"]);
+    assert_whitespace_only(input, &result);
 
     // Idempotency check: reflowing the result again should produce the same 
output
     // This was the bug - the second reflow would add a leading space
@@ -3610,11 +3554,11 @@
 
     let test_cases = vec![
         // (input, expected_first_line, expected_second_line)
-        ("**Bold one. Bold two.**", "**Bold one.**", "**Bold two.**"),
-        ("*Italic one. Italic two.*", "*Italic one.*", "*Italic two.*"),
-        ("~~Strike one. Strike two.~~", "~~Strike one.~~", "~~Strike two.~~"),
-        ("__Bold underscore. Second.__", "__Bold underscore.__", 
"__Second.__"),
-        ("_Italic underscore. Second._", "_Italic underscore._", "_Second._"),
+        ("**Bold one. Bold two.**", "**Bold one.", "Bold two.**"),
+        ("*Italic one. Italic two.*", "*Italic one.", "Italic two.*"),
+        ("~~Strike one. Strike two.~~", "~~Strike one.", "Strike two.~~"),
+        ("__Bold underscore. Second.__", "__Bold underscore.", "Second.__"),
+        ("_Italic underscore. Second._", "_Italic underscore.", "Second._"),
     ];
 
     for (input, expected_first, expected_second) in test_cases {
@@ -3622,6 +3566,7 @@
         assert_eq!(result.len(), 2, "Input {input:?} should produce 2 lines: 
{result:?}");
         assert_eq!(result[0], expected_first, "First line mismatch for 
{input:?}");
         assert_eq!(result[1], expected_second, "Second line mismatch for 
{input:?}");
+        assert_whitespace_only(input, &result);
 
         // Idempotency: reflow the result and verify it's unchanged
         let joined = result.join("\n");

++++++ rumdl.obsinfo ++++++
--- /var/tmp/diff_new_pack.rCFS8M/_old  2026-07-29 19:02:30.536389080 +0200
+++ /var/tmp/diff_new_pack.rCFS8M/_new  2026-07-29 19:02:30.540389218 +0200
@@ -1,5 +1,5 @@
 name: rumdl
-version: 0.2.44
-mtime: 1785229956
-commit: f4ec7f95f17043a4ddfeedeafef49a9f698e3845
+version: 0.2.45
+mtime: 1785264074
+commit: 0610735e0d36bc1cfd5a77cc720c08132c241182
 

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

Reply via email to