fix: render <br> between paragraphs as a visible section break (#2548)

Co-authored-by: Brooks Ilg <brooksmilg@gmail.com>
This commit is contained in:
Uri Tauber
2026-07-06 20:10:31 +03:00
committed by GitHub
co-authored by Brooks Ilg
parent ca1b833126
commit 44ff313740
4 changed files with 236 additions and 2 deletions
+8
View File
@@ -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 <br> element. Used by startNewTextBlock to inject
// a full line-height gap when the <br> 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 <br> block
// is merged with the following paragraph; never propagate it further.
result.fromBrElement = false;
return result;
}
@@ -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 <br> 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 <br> block stayed empty (i.e. no inline text was added).
const int16_t lineHeight = static_cast<int16_t>(renderer.getLineHeight(fontId) * lineCompression + 0.5f);
incoming.marginTop = static_cast<int16_t>(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 <br/> 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. <br> 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 <br> 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,