diff --git a/lib/Epub/Epub/blocks/BlockStyle.h b/lib/Epub/Epub/blocks/BlockStyle.h
index fbc18d42..08bcfd62 100644
--- a/lib/Epub/Epub/blocks/BlockStyle.h
+++ b/lib/Epub/Epub/blocks/BlockStyle.h
@@ -32,6 +32,11 @@ struct BlockStyle {
bool isRtl = false; // true if resolved direction is RTL
bool directionDefined = false; // true if direction was explicitly set in CSS/HTML
+ // Set when this block was created by a element. Used by startNewTextBlock to inject
+ // a full line-height gap when the block stays empty (section-break use case).
+ // NOT propagated through getCombinedBlockStyle so it can't leak into sibling blocks.
+ bool fromBrElement = false;
+
// Combined insets (margin + padding)
[[nodiscard]] int16_t leftInset() const { return marginLeft + paddingLeft; }
[[nodiscard]] int16_t rightInset() const { return marginRight + paddingRight; }
@@ -92,6 +97,9 @@ struct BlockStyle {
result.directionDefined = true;
}
+ // fromBrElement is consumed by startNewTextBlock when an empty block
+ // is merged with the following paragraph; never propagate it further.
+ result.fromBrElement = false;
return result;
}
diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
index b04159e7..7263de4a 100644
--- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
+++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
@@ -239,7 +239,16 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
// open. Merge those into the new style so the first child in a container inherits
// the container's vertical spacing.
const auto style = currentTextBlock->getBlockStyle();
- currentTextBlock->setBlockStyle(style.getCombinedBlockStyle(blockStyle, BlockStyle::CombineAxis::Vertical));
+ BlockStyle incoming = blockStyle;
+ if (style.fromBrElement) {
+ // The empty block was created by a section separator. Inject a full line of
+ // blank space before the following paragraph so the scene/section break is visible.
+ // This only fires when the block stayed empty (i.e. no inline text was added).
+ const int16_t lineHeight = static_cast(renderer.getLineHeight(fontId) * lineCompression + 0.5f);
+ incoming.marginTop = static_cast(incoming.marginTop + lineHeight);
+ }
+
+ currentTextBlock->setBlockStyle(style.getCombinedBlockStyle(incoming, BlockStyle::CombineAxis::Vertical));
flushPendingAnchor();
return;
@@ -855,7 +864,14 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
// flush word preceding to currentTextBlock before calling startNewTextBlock
self->flushPartWordBuffer();
}
- self->startNewTextBlock(self->blockStyleStack.back().withoutBottom());
+ // Tag the new block so startNewTextBlock can inject a full line-height gap if
+ // the block remains empty (i.e. is a section separator between paragraphs).
+ // If the block gets text added before the next block opens it becomes non-empty,
+ // goes through makePages() normally, and the flag has no effect (inline case).
+ BlockStyle brStyle =
+ self->currentTextBlock ? self->currentTextBlock->getBlockStyle() : self->blockStyleStack.back();
+ brStyle.fromBrElement = true;
+ self->startNewTextBlock(brStyle);
} else {
self->currentCssStyle = cssStyle;
const auto accumulated = self->blockStyleStack.back().getCombinedBlockStyle(userAlignmentBlockStyle,
diff --git a/scripts/generate_br_section_break_epub.py b/scripts/generate_br_section_break_epub.py
new file mode 100644
index 00000000..a22d4462
--- /dev/null
+++ b/scripts/generate_br_section_break_epub.py
@@ -0,0 +1,210 @@
+#!/usr/bin/env python3
+"""
+Generate a test EPUB for section-break rendering.
+
+Tests that a bare element between paragraphs produces a visible blank-line
+gap (section separator), while a inside a paragraph only produces a line
+break with no extra spacing.
+
+Cases covered:
+ 1. Standalone between paragraphs (section break — must show gap).
+ 2. with a CSS class (calibre-style section break).
+ 3. Multiple consecutive elements (each adds one line of spacing).
+ 4. Inline inside a
(line break only — no extra gap).
+ 5. at start of chapter (no gap before first paragraph).
+ 6. following a heading.
+
+Visual verification instructions are embedded as the first paragraph of each
+chapter so a human tester can confirm the expected result on device.
+"""
+
+import os
+import zipfile
+from pathlib import Path
+
+OUTPUT_DIR = Path(__file__).parent.parent / "test" / "epubs"
+OUTPUT_PATH = OUTPUT_DIR / "test_br_section_break.epub"
+
+FILLER = (
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod "
+ "tempor incididunt ut labore et dolore magna aliqua."
+)
+
+CSS = """\
+body { margin: 0; padding: 0; }
+p { margin-top: 1pt; margin-bottom: 0; text-indent: 1em; text-align: justify; }
+h1 { text-align: center; margin-top: 0.5em; margin-bottom: 0.5em; }
+h2 { text-align: center; margin-top: 0.5em; margin-bottom: 0.5em; }
+.section-br { display: block; }
+"""
+
+def xhtml(title, body):
+ return f"""\
+
+
+
+
PASS: Two blank lines should appear between the sections (one per <br>).
+
{FILLER}
+
+
+
{FILLER}
+
PASS: Three blank lines should appear below.
+
{FILLER}
+
+
+
+
{FILLER}
+""")
+
+# ---------------------------------------------------------------------------
+# Chapter 4 — inline inside a paragraph (line break, NOT a gap)
+# ---------------------------------------------------------------------------
+ch4 = xhtml("Ch4: Inline br", """
+
Ch 4: Inline <br> Inside a Paragraph
+
PASS: The two lines below should be adjacent with NO extra gap between them.
+The <br> is inside the paragraph and must only break the line.
+
First line of the paragraph. Second line of the paragraph — directly below, no gap.
+
PASS: Above should look like two closely-spaced lines, not like two paragraphs
+separated by a blank line.
+""")
+
+# ---------------------------------------------------------------------------
+# Chapter 5 — following a heading
+# ---------------------------------------------------------------------------
+ch5 = xhtml("Ch5: br after heading", f"""
+
Ch 5: <br> After a Heading
+
+
PASS: There should be a blank-line gap between the heading above and this paragraph.
+
{FILLER}
+
Section heading
+
+
PASS: There should be a blank-line gap between the section heading and this paragraph.
+""")
+
+# ---------------------------------------------------------------------------
+# Chapter 6 — at very start of chapter (no spurious leading gap)
+# ---------------------------------------------------------------------------
+ch6 = xhtml("Ch6: br at chapter start", f"""
+
Ch 6: <br> at Chapter Start
+
PASS: This heading should appear near the top of the page with no large blank
+area above it despite the <br> being the very first element.