Files
Crosspoint/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h
T

231 lines
9.7 KiB
C++

#pragma once
#include <Print.h>
#include <expat.h>
#include <climits>
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "../FootnoteEntry.h"
#include "../ParsedText.h"
#include "../blocks/ImageBlock.h"
#include "../blocks/TextBlock.h"
#include "../css/CssParser.h"
#include "../css/CssStyle.h"
class Page;
class GfxRenderer;
class Epub;
#define MAX_WORD_SIZE 200
class ChapterHtmlSlimParser final : public Print {
std::shared_ptr<Epub> epub;
GfxRenderer& renderer;
std::function<void(std::unique_ptr<Page>)> completePageFn;
std::function<void(int)> progressFn; // Progress callback (0-100)
int depth = 0;
int skipUntilDepth = INT_MAX;
int skipTextUntilDepth = INT_MAX; // skip character data inside synthetic zero-height spacer <p>
int boldUntilDepth = INT_MAX;
int italicUntilDepth = INT_MAX;
int underlineUntilDepth = INT_MAX;
int strikethroughUntilDepth = INT_MAX;
int preUntilDepth = INT_MAX; // set when inside a <pre> element; enables \n → line-break handling
// buffer for building up words from characters, will auto break if longer than this
// leave one char at end for null pointer
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
int partWordBufferIndex = 0;
bool nextWordContinues = false; // true when next flushed word attaches to previous (inline element boundary)
std::unique_ptr<ParsedText> currentTextBlock = nullptr;
std::unique_ptr<Page> currentPage = nullptr;
int16_t currentPageNextY = 0;
int fontId;
float lineCompression;
bool extraParagraphSpacing;
uint8_t paragraphAlignment;
uint16_t viewportWidth;
uint16_t viewportHeight;
bool hyphenationEnabled;
const CssParser* cssParser;
bool embeddedStyle;
uint8_t imageRendering;
std::string contentBase;
std::string imageBasePath;
int imageCounter = 0;
bool lowMemoryImageFallback = false;
// Style tracking (replaces depth-based approach)
struct StyleStackEntry {
int depth = 0;
bool hasBold = false, bold = false;
bool hasItalic = false, italic = false;
bool hasUnderline = false, underline = false;
bool hasStrikethrough = false, strikethrough = false;
bool hasSup = false, sup = false;
bool hasSub = false, sub = false;
};
std::vector<StyleStackEntry> inlineStyleStack;
CssStyle currentCssStyle;
bool effectiveBold = false;
bool effectiveItalic = false;
bool effectiveUnderline = false;
bool effectiveStrikethrough = false;
bool effectiveSup = false;
bool effectiveSub = false;
// Buffered table model — populated while inside <table>, emitted on </table>
struct BufferedTableCell {
std::unique_ptr<ParsedText> text;
bool isHeader = false;
};
struct BufferedTableRow {
std::vector<BufferedTableCell> cells;
bool isHeaderRow = false; // true when all cells in this row are <th>
};
struct BufferedTable {
std::vector<BufferedTableRow> rows;
int depth = 0; // nesting depth; > 1 means we're inside a nested table
bool unsupported = false; // true → emit as paragraphs instead of grid
};
std::unique_ptr<BufferedTable> currentTable;
BufferedTableCell* currentTableCell = nullptr; // non-null while inside <td>/<th>
struct ListEntry {
int depth;
bool isOrdered;
int counter;
};
std::vector<ListEntry> listStack;
// Anchor-to-page mapping: tracks which page each HTML id attribute lands on
int completedPageCount = 0;
std::vector<std::pair<std::string, uint16_t>> anchorData;
std::string pendingAnchorId; // deferred until after previous text block is flushed
std::vector<std::string> tocAnchors;
// Paragraph index tracking for XPath-to-page lookup table.
// Counts <p> sibling indices (1-based, matching XPath convention) during page building.
// Stored per page in the section cache so that XPath p[N] can be resolved to a page
// without reparsing, and current page can generate an XPath without reparsing.
uint16_t xpathParagraphIndex = 0; // current <p> sibling index (1-based)
// Running count of <li> elements opened anywhere in the chapter (1-based, any depth).
// Used by the section LUT so KOReader-supplied list-item XPaths can snap to the exact
// page on download, the same way <p>-anchored XPaths use xpathParagraphIndex.
uint16_t xpathListItemIndex = 0;
int xpathBodyDepth = -1; // depth of the <body> element (-1 = not yet seen)
// Byte offset of the most recent direct-body-child element start (any tag at xpathBodyDepth+1).
// Recorded at the same depth condition that increments xpathParagraphIndex, so the stored
// offset is guaranteed to land on a body-child element boundary. This keeps the XPath forward
// mapper's partial-parse heuristic reliable for wrapped chapters: without this, the offset
// could point mid-way into a nested <div>/<section>, which confuses partialBaseDepth.
uint32_t lastBodyChildByteOffset = 0;
struct ParagraphLutEntry {
uint32_t xhtmlByteOffset; // byte offset of most recent body-child element start at page break
uint16_t paragraphIndex; // 1-based <p> index at page completion
uint16_t listItemIndex; // running <li> count at page completion (any depth)
};
std::vector<ParagraphLutEntry> paragraphLutPerPage; // deep LUT: one entry per page
// Active parser handle during streaming, nullptr otherwise.
// Stored as a member so page-break sites (addLineToPage, image breaks) can call
// XML_GetCurrentByteIndex without needing the parser threaded through every call.
XML_Parser activeParser = nullptr;
// Streaming state for the Print-derived parsing API.
size_t totalStreamSize = 0;
size_t bytesStreamed = 0;
int lastReportedProgress = -1;
int progressStepPercent = 0;
bool progressUiEnabled = true;
bool streamFailed = false;
uint32_t streamStartTimeMs = 0;
// Footnote link tracking
bool insideFootnoteLink = false;
int footnoteLinkDepth = -1;
FootnoteEntry currentFootnote = {};
int currentFootnoteLinkTextLen = 0;
std::vector<std::pair<int, FootnoteEntry>> pendingFootnotes; // <wordIndex, entry>
int wordsExtractedInBlock = 0;
bool bionicReadingEnabled = false;
bool layoutFailed = false;
// Per-chapter caches: resolveStyle and parseInlineStyle are called for every HTML element;
// caching by (tag|classAttr) and styleAttr avoids repeated string operations and hash lookups.
std::unordered_map<std::string, CssStyle> cssStyleCache_;
std::unordered_map<std::string, CssStyle> inlineStyleCache_;
void updateEffectiveInlineStyle();
bool ensureHeapForTextLayout(const char* phase);
void startNewTextBlock(const BlockStyle& blockStyle);
bool flushPartWordBuffer();
void makePages();
void emitBufferedTable();
void emitTableAsFragments(BufferedTable& table);
void emitTableAsParagraphs(BufferedTable& table);
// Emit currentPage to the consumer while keeping paragraphLutPerPage and completedPageCount
// in lockstep. Every page break MUST go through this helper; open-coded completePageFn
// calls risk desynchronising paragraphLutPerPage and failing the size check in Section.cpp.
void emitPage(uint32_t xhtmlByteOffset);
// XML callbacks
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
static void XMLCALL defaultHandlerExpand(void* userData, const XML_Char* s, int len);
static void XMLCALL endElement(void* userData, const XML_Char* name);
public:
explicit ChapterHtmlSlimParser(
std::shared_ptr<Epub> epub, GfxRenderer& renderer, const int fontId, const float lineCompression,
const bool extraParagraphSpacing, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool bionicReadingEnabled,
const std::function<void(std::unique_ptr<Page>)>& completePageFn, const bool embeddedStyle,
const std::string& contentBase, const std::string& imageBasePath, const uint8_t imageRendering = 0,
std::vector<std::string> tocAnchors = {}, const std::function<void(int)>& progressFn = nullptr,
const CssParser* cssParser = nullptr)
: epub(epub),
renderer(renderer),
fontId(fontId),
lineCompression(lineCompression),
extraParagraphSpacing(extraParagraphSpacing),
paragraphAlignment(paragraphAlignment),
viewportWidth(viewportWidth),
viewportHeight(viewportHeight),
hyphenationEnabled(hyphenationEnabled),
bionicReadingEnabled(bionicReadingEnabled),
completePageFn(completePageFn),
progressFn(progressFn),
cssParser(cssParser),
embeddedStyle(embeddedStyle),
imageRendering(imageRendering),
contentBase(contentBase),
imageBasePath(imageBasePath),
tocAnchors(std::move(tocAnchors)) {}
~ChapterHtmlSlimParser() override;
// Streaming parse lifecycle. Caller flow:
// parser.setup(totalInflatedSize);
// epub->readItemContentsToStream(href, parser, ...);
// parser.finalize();
// Returns false from setup() on parser allocation failure; check streamSucceeded()
// after finalize() to detect a parse error mid-stream.
bool setup(size_t totalInflatedSize);
bool finalize();
[[nodiscard]] bool streamSucceeded() const { return !streamFailed; }
// Print interface — fed by Epub::readItemContentsToStream.
size_t write(uint8_t) override;
size_t write(const uint8_t* buffer, size_t size) override;
ParsedText::LineProcessResult addLineToPage(std::shared_ptr<TextBlock> line, bool lineEndsWithHyphenatedWord,
bool suppressHyphenationRetry);
const std::vector<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
const std::vector<ParagraphLutEntry>& getParagraphLutPerPage() const { return paragraphLutPerPage; }
};