Fix unwanted nbsp payload of empty paragraphs
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
#include <expat.h>
|
#include <expat.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
#include "../../Epub.h"
|
#include "../../Epub.h"
|
||||||
#include "../Page.h"
|
#include "../Page.h"
|
||||||
@@ -78,6 +79,28 @@ bool isTableStructuralTag(const char* name) {
|
|||||||
return strcmp(name, "table") == 0 || strcmp(name, "tr") == 0 || strcmp(name, "td") == 0 || strcmp(name, "th") == 0;
|
return strcmp(name, "table") == 0 || strcmp(name, "tr") == 0 || strcmp(name, "td") == 0 || strcmp(name, "th") == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calibre sometimes injects empty <p style="margin:0; border:0; height:0">...</p>
|
||||||
|
// spacers inside running prose. Keep them as paragraph boundaries, but ignore
|
||||||
|
// their inner text payload (usually NBSP) to avoid no-break-space glue artifacts.
|
||||||
|
bool isZeroHeightSpacerParagraph(const char* name, const std::string& styleAttr) {
|
||||||
|
if (strcmp(name, "p") != 0 || styleAttr.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string normalized;
|
||||||
|
normalized.reserve(styleAttr.size());
|
||||||
|
for (const char ch : styleAttr) {
|
||||||
|
if (!isWhitespace(ch)) {
|
||||||
|
normalized.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool hasZeroHeight = normalized.find("height:0") != std::string::npos;
|
||||||
|
const bool hasZeroMargin = normalized.find("margin:0") != std::string::npos;
|
||||||
|
const bool hasZeroBorder = normalized.find("border:0") != std::string::npos;
|
||||||
|
return hasZeroHeight && hasZeroMargin && hasZeroBorder;
|
||||||
|
}
|
||||||
|
|
||||||
// Update effective bold/italic/underline based on block style and inline style stack
|
// Update effective bold/italic/underline based on block style and inline style stack
|
||||||
void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
||||||
// Start with block-level styles
|
// Start with block-level styles
|
||||||
@@ -653,6 +676,22 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
||||||
|
if (isZeroHeightSpacerParagraph(name, styleAttr)) {
|
||||||
|
// Preserve paragraph break semantics for this <p>, but skip its inner text payload.
|
||||||
|
self->currentCssStyle = cssStyle;
|
||||||
|
auto blockStyle = userAlignmentBlockStyle;
|
||||||
|
if (self->embeddedStyle && cssStyle.hasTextAlign()) {
|
||||||
|
blockStyle.alignment = cssStyle.textAlign;
|
||||||
|
blockStyle.textAlignDefined = true;
|
||||||
|
}
|
||||||
|
self->startNewTextBlock(blockStyle);
|
||||||
|
self->updateEffectiveInlineStyle();
|
||||||
|
|
||||||
|
self->skipTextUntilDepth = self->depth;
|
||||||
|
self->depth += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (strcmp(name, "br") == 0) {
|
if (strcmp(name, "br") == 0) {
|
||||||
if (self->partWordBufferIndex > 0) {
|
if (self->partWordBufferIndex > 0) {
|
||||||
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
||||||
@@ -800,6 +839,11 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore character data inside synthetic zero-height spacer <p> tags.
|
||||||
|
if (self->skipTextUntilDepth < self->depth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Collect footnote link display text (for the number label)
|
// Collect footnote link display text (for the number label)
|
||||||
// Skip whitespace and brackets to normalize noterefs like "[1]" → "1"
|
// Skip whitespace and brackets to normalize noterefs like "[1]" → "1"
|
||||||
if (self->insideFootnoteLink) {
|
if (self->insideFootnoteLink) {
|
||||||
@@ -1050,6 +1094,11 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
self->skipUntilDepth = INT_MAX;
|
self->skipUntilDepth = INT_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Leaving zero-height spacer paragraph text-skip scope
|
||||||
|
if (self->skipTextUntilDepth == self->depth) {
|
||||||
|
self->skipTextUntilDepth = INT_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
if (self->tableDepth == 1 && (strcmp(name, "td") == 0 || strcmp(name, "th") == 0)) {
|
if (self->tableDepth == 1 && (strcmp(name, "td") == 0 || strcmp(name, "th") == 0)) {
|
||||||
self->nextWordContinues = false;
|
self->nextWordContinues = false;
|
||||||
}
|
}
|
||||||
@@ -1104,9 +1153,11 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
// Margins/padding are preserved so parent element spacing still accumulates correctly.
|
// Margins/padding are preserved so parent element spacing still accumulates correctly.
|
||||||
if (self->currentTextBlock && self->currentTextBlock->isEmpty()) {
|
if (self->currentTextBlock && self->currentTextBlock->isEmpty()) {
|
||||||
auto style = self->currentTextBlock->getBlockStyle();
|
auto style = self->currentTextBlock->getBlockStyle();
|
||||||
// Keep alignment for synthetic empty <br> separator blocks so following inline
|
// Keep alignment only when closing the <br> separator itself so subsequent text
|
||||||
// text after <br/> inside centered/right-aligned containers preserves alignment.
|
// within the same block container stays aligned. Reset alignment when closing
|
||||||
if (!style.fromBrElement) {
|
// other block tags (e.g. div/p) to avoid leaking centered/right alignment globally.
|
||||||
|
const bool preserveForBrClose = style.fromBrElement && strcmp(name, "br") == 0;
|
||||||
|
if (!preserveForBrClose) {
|
||||||
style.textAlignDefined = false;
|
style.textAlignDefined = false;
|
||||||
style.alignment = (self->paragraphAlignment == static_cast<uint8_t>(CssTextAlign::None))
|
style.alignment = (self->paragraphAlignment == static_cast<uint8_t>(CssTextAlign::None))
|
||||||
? CssTextAlign::Justify
|
? CssTextAlign::Justify
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class ChapterHtmlSlimParser {
|
|||||||
std::function<void(int)> progressFn; // Progress callback (0-100)
|
std::function<void(int)> progressFn; // Progress callback (0-100)
|
||||||
int depth = 0;
|
int depth = 0;
|
||||||
int skipUntilDepth = INT_MAX;
|
int skipUntilDepth = INT_MAX;
|
||||||
|
int skipTextUntilDepth = INT_MAX; // skip character data inside synthetic zero-height spacer <p>
|
||||||
int boldUntilDepth = INT_MAX;
|
int boldUntilDepth = INT_MAX;
|
||||||
int italicUntilDepth = INT_MAX;
|
int italicUntilDepth = INT_MAX;
|
||||||
int underlineUntilDepth = INT_MAX;
|
int underlineUntilDepth = INT_MAX;
|
||||||
|
|||||||
Reference in New Issue
Block a user