diff --git a/epub2md/html_converter.py b/epub2md/html_converter.py
index 2a2717b..ce84500 100644
--- a/epub2md/html_converter.py
+++ b/epub2md/html_converter.py
@@ -152,6 +152,26 @@ def indent_block(text: str, first: str, rest: str) -> str:
return "\n".join(out)
+def _collapse_blank_lines(text: str) -> str:
+ """Reduce runs of blank lines to one, except inside fenced code blocks (also indented/quoted ones)."""
+ out: list[str] = []
+ fence = ""
+ for line in text.split("\n"):
+ bare = re.sub(r"^[ \t]*(?:>[ \t]?)*[ \t]*", "", line)
+ if fence:
+ if bare.rstrip() and set(bare.rstrip()) == {fence[0]} and len(bare.rstrip()) >= len(fence):
+ fence = ""
+ out.append(line)
+ continue
+ m = re.match(r"(`{3,}|~{3,})", bare)
+ if m:
+ fence = m.group(1)
+ elif not line.strip() and out and not out[-1].strip():
+ continue
+ out.append(line)
+ return "\n".join(out)
+
+
_BLOCK_SYNTAX = ("```", "~~~", "|", "
", "#", "$$", "\\[", "* * *")
@@ -251,7 +271,7 @@ class Renderer:
blocks.extend(self.render_items(ch.items))
blocks.extend(self._chapter_note_definitions())
text = "\n\n".join(b for b in blocks if b.strip())
- return re.sub(r"\n{3,}", "\n\n", text).strip() + "\n"
+ return _collapse_blank_lines(text).strip() + "\n"
def render_items(self, items: Sequence[tuple[str | None, Any]]) -> list[str]:
blocks: list[str] = []
diff --git a/tests/test_qa_regressions.py b/tests/test_qa_regressions.py
index efe605a..45ecd40 100644
--- a/tests/test_qa_regressions.py
+++ b/tests/test_qa_regressions.py
@@ -98,3 +98,12 @@ def test_inline_svg_inside_html_table_is_html_img(convert):
assert "![" not in table, table
assert re.search(r'
', table), table
assert run.report["output_link_check"]["broken_count"] == 0
+
+
+def test_multiple_blank_lines_inside_code_are_kept(convert):
+ """QA2-06: runs of blank lines inside code blocks were collapsed to one (PEP 8: two between functions)."""
+ code = "def f():\n pass\n\n\nclass K:\n pass\n\n\n\nx = 1"
+ body = f"K
Vorher
{code}Nachher
"
+ text = convert(Book(docs=[Doc("c.xhtml", body)])).chapter("001")
+ assert f"```python\n{code}\n```" in text, text
+ assert "\n\n\n" not in text.replace(code, "") # outside code blank lines are still normalized