diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index bf0e2159..2777a871 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -1,6 +1,5 @@ #include "Page.h" -#include #include #include @@ -49,135 +48,6 @@ std::unique_ptr PageImage::deserialize(FsFile& file) { return std::unique_ptr(new PageImage(std::move(ib), xPos, yPos)); } -void PageTableFragment::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) { - const int drawX = xPos + xOffset; - const int drawY = yPos + yOffset; - - // Outer border - renderer.drawRect(drawX, drawY, totalWidth, totalHeight, true); - - // Vertical column separators - int colX = drawX; - for (uint8_t c = 0; c < columnCount - 1; c++) { - colX += colWidths[c]; - renderer.drawLine(colX, drawY, colX, drawY + totalHeight - 1, true); - } - - // Rows: text content + horizontal separators - int rowY = drawY; - for (size_t r = 0; r < rows.size(); r++) { - const TableRow& row = rows[r]; - int cellX = drawX; - for (uint8_t c = 0; c < columnCount && c < static_cast(row.cells.size()); c++) { - const TableCell& cell = row.cells[c]; - int lineY = rowY + TABLE_CELL_PADDING; - for (const auto& line : cell.lines) { - line->render(renderer, fontId, cellX + TABLE_CELL_PADDING, lineY); - lineY += renderer.getLineHeight(fontId); - } - cellX += colWidths[c]; - } - rowY += row.height; - // Draw horizontal separator (skip after last row — outer border covers it) - if (r + 1 < rows.size()) { - const int sepLineWidth = row.isHeaderRow ? 2 : 1; - renderer.drawLine(drawX, rowY, drawX + totalWidth - 1, rowY, sepLineWidth, true); - } - } -} - -bool PageTableFragment::serialize(FsFile& file) { - serialization::writePod(file, xPos); - serialization::writePod(file, yPos); - serialization::writePod(file, columnCount); - serialization::writePod(file, totalWidth); - serialization::writePod(file, totalHeight); - for (uint8_t c = 0; c < MAX_TABLE_COLS; c++) { - serialization::writePod(file, colWidths[c]); - } - const uint16_t rowCount = static_cast(rows.size()); - serialization::writePod(file, rowCount); - for (const auto& row : rows) { - serialization::writePod(file, row.height); - serialization::writePod(file, row.isHeaderRow); - const uint8_t cellCount = static_cast(row.cells.size()); - serialization::writePod(file, cellCount); - for (const auto& cell : row.cells) { - serialization::writePod(file, cell.isHeader); - const uint8_t lineCount = static_cast(cell.lines.size()); - serialization::writePod(file, lineCount); - for (const auto& line : cell.lines) { - if (!line->serialize(file)) return false; - } - } - } - return true; -} - -std::unique_ptr PageTableFragment::deserialize(FsFile& file) { - int16_t xPos, yPos; - serialization::readPod(file, xPos); - serialization::readPod(file, yPos); - - uint8_t columnCount; - uint16_t totalWidth, totalHeight; - serialization::readPod(file, columnCount); - serialization::readPod(file, totalWidth); - serialization::readPod(file, totalHeight); - - if (columnCount == 0 || columnCount > MAX_TABLE_COLS) { - LOG_ERR("PGE", "TableFragment: invalid columnCount %u", columnCount); - return nullptr; - } - - std::array colWidths = {}; - for (uint8_t c = 0; c < MAX_TABLE_COLS; c++) { - serialization::readPod(file, colWidths[c]); - } - - uint16_t rowCount; - serialization::readPod(file, rowCount); - if (rowCount > MAX_TABLE_ROWS) { - LOG_ERR("PGE", "TableFragment: invalid rowCount %u", rowCount); - return nullptr; - } - - std::vector rows; - rows.reserve(rowCount); - for (uint16_t r = 0; r < rowCount; r++) { - TableRow row; - serialization::readPod(file, row.height); - serialization::readPod(file, row.isHeaderRow); - uint8_t cellCount; - serialization::readPod(file, cellCount); - if (cellCount > MAX_TABLE_COLS) { - LOG_ERR("PGE", "TableFragment: invalid cellCount %u in row %u", cellCount, r); - return nullptr; - } - row.cells.reserve(cellCount); - for (uint8_t c = 0; c < cellCount; c++) { - TableCell cell; - serialization::readPod(file, cell.isHeader); - uint8_t lineCount; - serialization::readPod(file, lineCount); - cell.lines.reserve(lineCount); - for (uint8_t l = 0; l < lineCount; l++) { - auto tb = TextBlock::deserialize(file); - if (!tb) { - LOG_ERR("PGE", "TableFragment: TextBlock deserialize failed at row %u cell %u line %u", r, c, l); - return nullptr; - } - cell.lines.push_back(std::move(tb)); - } - row.cells.push_back(std::move(cell)); - } - rows.push_back(std::move(row)); - } - - return std::unique_ptr( - new PageTableFragment(columnCount, totalWidth, totalHeight, colWidths, std::move(rows), xPos, yPos)); -} - void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const { for (auto& element : elements) { element->render(renderer, fontId, xOffset, yOffset); @@ -229,10 +99,6 @@ std::unique_ptr Page::deserialize(FsFile& file) { } else if (tag == TAG_PageImage) { auto pi = PageImage::deserialize(file); page->elements.push_back(std::move(pi)); - } else if (tag == TAG_PageTable) { - auto pt = PageTableFragment::deserialize(file); - if (!pt) return nullptr; - page->elements.push_back(std::move(pt)); } else { LOG_ERR("PGE", "Deserialization failed: Unknown tag %u", tag); return nullptr; diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 8b91fb92..46814b15 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -11,16 +10,9 @@ #include "blocks/ImageBlock.h" #include "blocks/TextBlock.h" -static constexpr uint8_t MAX_TABLE_COLS = 8; -static constexpr uint16_t MAX_TABLE_ROWS = 48; -static constexpr uint8_t TABLE_CELL_PADDING = 5; -static constexpr uint16_t MIN_COL_INNER_WIDTH = 24; -static constexpr uint8_t MAX_CELL_LINES = 64; - enum PageElementTag : uint8_t { TAG_PageLine = 1, - TAG_PageImage = 2, - TAG_PageTable = 3, + TAG_PageImage = 2, // New tag }; // represents something that has been added to a page @@ -63,42 +55,6 @@ class PageImage final : public PageElement { const ImageBlock& getImageBlock() const { return *imageBlock; } }; -struct TableCell { - std::vector> lines; - bool isHeader = false; -}; - -struct TableRow { - std::vector cells; - uint16_t height = 0; // pixel height including 2×CELL_PADDING - bool isHeaderRow = false; // drives 2px separator below this row -}; - -class PageTableFragment final : public PageElement { - uint8_t columnCount = 0; - uint16_t totalWidth = 0; - uint16_t totalHeight = 0; - std::array colWidths = {}; - std::vector rows; - - public: - PageTableFragment(uint8_t colCount, uint16_t totalWidth, uint16_t totalHeight, - std::array colWidths, std::vector rows, int16_t xPos, - int16_t yPos) - : PageElement(xPos, yPos), - columnCount(colCount), - totalWidth(totalWidth), - totalHeight(totalHeight), - colWidths(colWidths), - rows(std::move(rows)) {} - - void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override; - bool serialize(FsFile& file) override; - static std::unique_ptr deserialize(FsFile& file); - PageElementTag getTag() const override { return TAG_PageTable; } - uint16_t getTotalHeight() const { return totalHeight; } -}; - class Page { public: // the list of block index and line numbers on this page diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index be8fe1f2..806433a8 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -12,7 +12,7 @@ #include "parsers/ChapterHtmlSlimParser.h" namespace { -constexpr uint8_t SECTION_FILE_VERSION = 25; +constexpr uint8_t SECTION_FILE_VERSION = 24; constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION sizeof(int) + // fontId sizeof(float) + // lineCompression diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index c844f54f..132a29c9 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -188,29 +188,25 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() { fontStyle = static_cast(fontStyle | EpdFontFamily::STRIKETHROUGH); } - // flush the buffer — route to table cell text when inside a / + // flush the buffer partWordBuffer[partWordBufferIndex] = '\0'; - if (currentTableCell) { - currentTableCell->text->addWord(partWordBuffer, fontStyle, false, nextWordContinues); - } else if (currentTextBlock) { - currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues); - - if (currentTextBlock->size() > 96) { - LOG_DBG("EHP", "Text block too long, splitting into multiple pages"); - const int horizontalInset = currentTextBlock->getBlockStyle().totalHorizontalInset(); - const uint16_t effectiveWidth = - (horizontalInset < viewportWidth) ? static_cast(viewportWidth - horizontalInset) : viewportWidth; - currentTextBlock->layoutAndExtractLines( - renderer, fontId, effectiveWidth, - [this](const std::shared_ptr& textBlock, const bool lineEndsWithHyphenatedWord, - const bool suppressHyphenationRetry) { - return addLineToPage(textBlock, lineEndsWithHyphenatedWord, suppressHyphenationRetry); - }, - false); - } - } + currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues); partWordBufferIndex = 0; nextWordContinues = false; + + if (currentTextBlock->size() > 96) { + LOG_DBG("EHP", "Text block too long, splitting into multiple pages"); + const int horizontalInset = currentTextBlock->getBlockStyle().totalHorizontalInset(); + const uint16_t effectiveWidth = + (horizontalInset < viewportWidth) ? static_cast(viewportWidth - horizontalInset) : viewportWidth; + currentTextBlock->layoutAndExtractLines( + renderer, fontId, effectiveWidth, + [this](const std::shared_ptr& textBlock, const bool lineEndsWithHyphenatedWord, + const bool suppressHyphenationRetry) { + return addLineToPage(textBlock, lineEndsWithHyphenatedWord, suppressHyphenationRetry); + }, + false); + } } // Emit the current page, keeping paragraphLutPerPage and completedPageCount in lockstep. @@ -345,54 +341,65 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* return; } - // Buffered table rendering: accumulate cells in memory, emit as PageTableFragment on . + // Special handling for tables/cells: flatten into per-cell paragraphs with a prefixed header. if (strcmp(name, "table") == 0) { - if (self->currentTable) { - // Nested table — mark unsupported and track depth - self->currentTable->depth += 1; - self->currentTable->unsupported = true; - self->depth += 1; + // skip nested tables + if (self->tableDepth > 0) { + self->tableDepth += 1; return; } - // Flush any pending text before starting the table + if (self->partWordBufferIndex > 0) { self->flushPartWordBuffer(); } - if (self->currentTextBlock && !self->currentTextBlock->isEmpty()) { - self->makePages(); - } - self->currentTable = std::unique_ptr(new BufferedTable()); - self->currentTable->depth = 1; + self->tableDepth += 1; + self->tableRowIndex = 0; + self->tableColIndex = 0; self->depth += 1; return; } - if (self->currentTable && self->currentTable->depth == 1 && strcmp(name, "tr") == 0) { - self->currentTable->rows.emplace_back(); - if (self->currentTable->rows.size() > MAX_TABLE_ROWS) { - self->currentTable->unsupported = true; - } + if (self->tableDepth == 1 && strcmp(name, "tr") == 0) { + self->tableRowIndex += 1; + self->tableColIndex = 0; self->depth += 1; return; } - if (self->currentTable && self->currentTable->depth == 1 && (strcmp(name, "td") == 0 || strcmp(name, "th") == 0)) { + if (self->tableDepth == 1 && (strcmp(name, "td") == 0 || strcmp(name, "th") == 0)) { if (self->partWordBufferIndex > 0) { self->flushPartWordBuffer(); } - if (self->currentTable->rows.empty()) { - self->currentTable->rows.emplace_back(); + self->tableColIndex += 1; + + auto tableCellBlockStyle = BlockStyle(); + tableCellBlockStyle.textAlignDefined = true; + const auto align = (self->paragraphAlignment == static_cast(CssTextAlign::None)) + ? CssTextAlign::Justify + : static_cast(self->paragraphAlignment); + tableCellBlockStyle.alignment = align; + self->startNewTextBlock(tableCellBlockStyle); + + const std::string headerText = + "Tab Row " + std::to_string(self->tableRowIndex) + ", Cell " + std::to_string(self->tableColIndex) + ":"; + StyleStackEntry headerStyle; + headerStyle.depth = self->depth; + headerStyle.hasBold = true; + headerStyle.bold = false; + headerStyle.hasItalic = true; + headerStyle.italic = true; + headerStyle.hasUnderline = true; + headerStyle.underline = false; + self->inlineStyleStack.push_back(headerStyle); + self->updateEffectiveInlineStyle(); + self->characterData(userData, headerText.c_str(), static_cast(headerText.length())); + if (self->partWordBufferIndex > 0) { + self->flushPartWordBuffer(); } - BufferedTableRow& row = self->currentTable->rows.back(); - if (row.cells.size() >= MAX_TABLE_COLS) { - self->currentTable->unsupported = true; - } - const bool isHeader = (strcmp(name, "th") == 0); - row.cells.emplace_back(); - row.cells.back().isHeader = isHeader; - row.cells.back().text = - std::unique_ptr(new ParsedText(false, false)); // no paragraph spacing, no hyphenation in cells - self->currentTableCell = &row.cells.back(); + self->nextWordContinues = false; + self->inlineStyleStack.pop_back(); + self->updateEffectiveInlineStyle(); + self->depth += 1; return; } @@ -1086,18 +1093,11 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char* s, const int len) { auto* self = static_cast(userData); - // Skip content of nested tables (depth > 1 means we're inside a nested table) - if (self->currentTable && self->currentTable->depth > 1) { + // Skip content of nested table + if (self->tableDepth > 1) { return; } - // Route character data into the active table cell's ParsedText - if (self->currentTableCell) { - // Use the existing partWordBuffer + word-level accumulation logic below, - // but the flush target will be currentTableCell->text (handled in flushPartWordBuffer). - // Fall through to the normal character accumulation path. - } - // Middle of skip if (self->skipUntilDepth < self->depth) { return; @@ -1312,10 +1312,11 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n const bool headerOrBlockTag = isHeaderOrBlock(name); const bool tableStructuralTag = isTableStructuralTag(name); - if (self->currentTable && self->currentTable->depth > 1 && strcmp(name, "table") == 0) { + if (self->tableDepth > 1 && strcmp(name, "table") == 0) { + // get rid of all text inside the nested table self->partWordBufferIndex = 0; - self->currentTable->depth -= 1; - LOG_DBG("EHP", "nested table end, depth now %d", self->currentTable->depth); + self->tableDepth -= 1; + LOG_DBG("EHP", "nested table detected, get rid of its content"); return; } @@ -1371,37 +1372,18 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n self->skipTextUntilDepth = INT_MAX; } - if (self->currentTable && self->currentTable->depth == 1 && (strcmp(name, "td") == 0 || strcmp(name, "th") == 0)) { - if (self->partWordBufferIndex > 0) { - self->flushPartWordBuffer(); - } - // Determine if the whole row consists of header cells - if (!self->currentTable->rows.empty()) { - auto& row = self->currentTable->rows.back(); - bool allHeaders = !row.cells.empty(); - for (const auto& c : row.cells) { - if (!c.isHeader) { - allHeaders = false; - break; - } - } - row.isHeaderRow = allHeaders; - } - self->currentTableCell = nullptr; + if (self->tableDepth == 1 && (strcmp(name, "td") == 0 || strcmp(name, "th") == 0)) { self->nextWordContinues = false; } - if (self->currentTable && self->currentTable->depth == 1 && strcmp(name, "tr") == 0) { + if (self->tableDepth == 1 && (strcmp(name, "tr") == 0)) { self->nextWordContinues = false; } - if (self->currentTable && self->currentTable->depth == 1 && strcmp(name, "table") == 0) { - if (self->partWordBufferIndex > 0) { - self->flushPartWordBuffer(); - } - self->currentTableCell = nullptr; - self->emitBufferedTable(); - self->currentTable.reset(); + if (self->tableDepth == 1 && strcmp(name, "table") == 0) { + self->tableDepth -= 1; + self->tableRowIndex = 0; + self->tableColIndex = 0; self->nextWordContinues = false; } @@ -1711,197 +1693,3 @@ void ChapterHtmlSlimParser::makePages() { currentPageNextY += lineHeight / 2; } } - -// Guard: minimum free heap before attempting table layout (cell wrapping allocates TextBlock vectors) -static constexpr size_t MIN_FREE_HEAP_FOR_TABLE = 20 * 1024; - -void ChapterHtmlSlimParser::emitBufferedTable() { - if (!currentTable) return; - - if (currentTable->unsupported || currentTable->rows.empty()) { - LOG_DBG("EHP", "Table unsupported or empty — falling back to paragraph mode"); - emitTableAsParagraphs(*currentTable); - return; - } - - if (ESP.getFreeHeap() < MIN_FREE_HEAP_FOR_TABLE) { - LOG_ERR("EHP", "Low heap (%u), falling back to paragraph mode for table", ESP.getFreeHeap()); - emitTableAsParagraphs(*currentTable); - return; - } - - emitTableAsFragments(*currentTable); -} - -void ChapterHtmlSlimParser::emitTableAsFragments(BufferedTable& table) { - // Determine column count (max cells in any row) - uint8_t columnCount = 0; - for (const auto& row : table.rows) { - if (row.cells.size() > columnCount) { - columnCount = static_cast(row.cells.size()); - } - } - if (columnCount == 0 || columnCount > MAX_TABLE_COLS) { - emitTableAsParagraphs(table); - return; - } - - const uint16_t totalWidth = viewportWidth; - const uint16_t colWidth = totalWidth / columnCount; - const uint16_t innerColWidth = - (colWidth > 2 * TABLE_CELL_PADDING) ? static_cast(colWidth - 2 * TABLE_CELL_PADDING) : 0; - if (innerColWidth < MIN_COL_INNER_WIDTH) { - LOG_DBG("EHP", "Table columns too narrow (%u px inner) — falling back to paragraphs", innerColWidth); - emitTableAsParagraphs(table); - return; - } - - std::array colWidths = {}; - for (uint8_t c = 0; c < columnCount; c++) { - colWidths[c] = colWidth; - } - - const int lineHeight = static_cast(renderer.getLineHeight(fontId) * lineCompression + 0.5f); - - // Pre-wrap all cells and compute row heights - struct LayoutRow { - std::vector cells; - uint16_t height = 0; - bool isHeaderRow = false; - }; - std::vector layoutRows; - layoutRows.reserve(table.rows.size()); - - for (auto& bufRow : table.rows) { - LayoutRow lr; - lr.isHeaderRow = bufRow.isHeaderRow; - lr.cells.reserve(bufRow.cells.size()); - uint16_t maxLines = 0; - - for (auto& bufCell : bufRow.cells) { - TableCell cell; - cell.isHeader = bufCell.isHeader; - - if (bufCell.text && !bufCell.text->isEmpty()) { - // Wrap cell text to inner column width, collecting resulting TextBlock lines - bufCell.text->layoutAndExtractLines(renderer, fontId, innerColWidth, - [&cell](const std::shared_ptr& tb, bool, bool) { - if (cell.lines.size() < MAX_CELL_LINES) { - cell.lines.push_back(tb); - } - return ParsedText::LineProcessResult::Accepted; - }); - } - - if (cell.lines.size() > maxLines) { - maxLines = static_cast(cell.lines.size()); - } - lr.cells.push_back(std::move(cell)); - } - - // Pad rows that have fewer cells than columnCount with empty cells - while (lr.cells.size() < columnCount) { - lr.cells.emplace_back(); - } - - lr.height = static_cast(maxLines * lineHeight + 2 * TABLE_CELL_PADDING); - if (lr.height == 0) lr.height = static_cast(lineHeight + 2 * TABLE_CELL_PADDING); - layoutRows.push_back(std::move(lr)); - } - - // Ensure page is initialised - if (!currentPage) { - currentPage.reset(new Page()); - currentPageNextY = 0; - } - - // Greedily pack rows into fragments, page-breaking between fragments - std::vector fragmentRows; - uint16_t fragmentHeight = 0; - - auto emitFragment = [&]() { - if (fragmentRows.empty()) return; - - // Total height = sum of row heights + top border (1px) + bottom border included in outer rect - const uint16_t fragTotalHeight = static_cast(fragmentHeight + 1); // +1 for top border pixel - - // If this fragment won't fit on the current page, page-break first - if (currentPageNextY + fragTotalHeight > viewportHeight && currentPageNextY > 0) { - emitPage(lastBodyChildByteOffset); - } - - auto fragment = std::make_shared(columnCount, totalWidth, fragTotalHeight, colWidths, - std::move(fragmentRows), - /*xPos=*/0, /*yPos=*/static_cast(currentPageNextY)); - currentPage->elements.push_back(fragment); - currentPageNextY += fragTotalHeight; - fragmentRows.clear(); - fragmentHeight = 0; - }; - - for (auto& lr : layoutRows) { - // If a single row is taller than the full viewport, fall back for this row - if (lr.height > viewportHeight) { - // Emit whatever we have so far - emitFragment(); - // Emit this oversized row as a paragraph fallback - BufferedTable singleRowFallback; - BufferedTableRow fbRow; - fbRow.isHeaderRow = lr.isHeaderRow; - for (auto& cell : lr.cells) { - BufferedTableCell fbc; - fbc.isHeader = cell.isHeader; - // Re-create a minimal ParsedText from the already-wrapped lines - // by emitting each line's words as a new ParsedText paragraph - fbc.text = std::unique_ptr(new ParsedText(false, false)); - for (const auto& line : cell.lines) { - for (const auto& word : line->getWords()) { - fbc.text->addWord(word, EpdFontFamily::REGULAR, false, false); - } - } - fbRow.cells.push_back(std::move(fbc)); - } - singleRowFallback.rows.push_back(std::move(fbRow)); - emitTableAsParagraphs(singleRowFallback); - continue; - } - - const uint16_t rowContrib = static_cast(lr.height + 1); // +1 for separator line - - if (!fragmentRows.empty() && currentPageNextY + fragmentHeight + rowContrib > viewportHeight) { - emitFragment(); - } - - TableRow tr; - tr.isHeaderRow = lr.isHeaderRow; - tr.height = lr.height; - tr.cells = std::move(lr.cells); - fragmentRows.push_back(std::move(tr)); - fragmentHeight += rowContrib; - } - - emitFragment(); -} - -void ChapterHtmlSlimParser::emitTableAsParagraphs(BufferedTable& table) { - // Emit each cell as a sequential paragraph (content-preserving fallback) - for (auto& row : table.rows) { - for (auto& cell : row.cells) { - if (!cell.text || cell.text->isEmpty()) continue; - auto cellBlockStyle = BlockStyle(); - cellBlockStyle.textAlignDefined = true; - cellBlockStyle.alignment = (paragraphAlignment == static_cast(CssTextAlign::None)) - ? CssTextAlign::Justify - : static_cast(paragraphAlignment); - // Re-use the existing paragraph pipeline by moving the cell text into currentTextBlock - startNewTextBlock(cellBlockStyle); - // Transfer words from the buffered cell text into the new currentTextBlock - // by re-running layout directly - cell.text->layoutAndExtractLines( - renderer, fontId, viewportWidth, - [this](const std::shared_ptr& tb, bool lineEndsWithHyphen, bool suppressRetry) { - return addLineToPage(tb, lineEndsWithHyphen, suppressRetry); - }); - } - } -} diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 30467157..5e5e4250 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -73,22 +73,9 @@ class ChapterHtmlSlimParser final : public Print { bool effectiveItalic = false; bool effectiveUnderline = false; bool effectiveStrikethrough = false; - // Buffered table model — populated while inside , emitted on
- struct BufferedTableCell { - std::unique_ptr text; - bool isHeader = false; - }; - struct BufferedTableRow { - std::vector cells; - bool isHeaderRow = false; // true when all cells in this row are - }; - struct BufferedTable { - std::vector 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 currentTable; - BufferedTableCell* currentTableCell = nullptr; // non-null while inside / + int tableDepth = 0; + int tableRowIndex = 0; + int tableColIndex = 0; struct ListEntry { int depth; @@ -153,9 +140,6 @@ class ChapterHtmlSlimParser final : public Print { void startNewTextBlock(const BlockStyle& blockStyle); void 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. diff --git a/lib/Md/MdParser.cpp b/lib/Md/MdParser.cpp index 117b4aaf..2dc61592 100644 --- a/lib/Md/MdParser.cpp +++ b/lib/Md/MdParser.cpp @@ -234,47 +234,6 @@ std::vector parseInline(const std::string& text) { return spans; } -// Split a pipe-table row into trimmed cell strings. -// Input: "| foo | **bar** |" → ["foo", "**bar**"] -static std::vector splitTableCells(const std::string& line) { - std::vector cells; - size_t i = 0; - // Skip optional leading pipe - if (i < line.size() && line[i] == '|') i++; - while (i < line.size()) { - size_t start = i; - // Find next unescaped pipe - while (i < line.size() && !(line[i] == '|' && (i == 0 || line[i - 1] != '\\'))) i++; - std::string cell = line.substr(start, i - start); - // Trim whitespace - size_t cs = 0, ce = cell.size(); - while (cs < ce && (cell[cs] == ' ' || cell[cs] == '\t')) cs++; - while (ce > cs && (cell[ce - 1] == ' ' || cell[ce - 1] == '\t')) ce--; - cells.push_back(cell.substr(cs, ce - cs)); - if (i < line.size()) i++; // skip the pipe - } - // Drop trailing empty cell produced by trailing pipe - if (!cells.empty() && cells.back().empty()) cells.pop_back(); - return cells; -} - -bool isTableRow(const std::string& line) { - const std::string& t = line; - size_t i = 0; - while (i < t.size() && (t[i] == ' ' || t[i] == '\t')) i++; - return i < t.size() && t[i] == '|'; -} - -bool isTableSeparator(const std::string& line) { - if (!isTableRow(line)) return false; - // Every non-pipe, non-space character must be - or : - for (char c : line) { - if (c != '|' && c != '-' && c != ':' && c != ' ' && c != '\t') return false; - } - // Must contain at least one - - return line.find('-') != std::string::npos; -} - bool isCodeFence(const std::string& line) { auto trimmed = trimLeft(line); if (trimmed.size() < 3) return false; @@ -423,20 +382,6 @@ ParsedLine parseLine(const std::string& rawLine, bool inCodeBlock) { return result; } - // GFM pipe table - if (isTableRow(trimmed)) { - if (isTableSeparator(trimmed)) { - result.blockType = BlockType::TableSeparator; - return result; - } - result.blockType = BlockType::TableRow; // promoted to TableHeader by caller if needed - auto cells = splitTableCells(trimmed); - for (auto& cell : cells) { - result.tableCells.push_back(parseInline(cell)); - } - return result; - } - // Default: paragraph result.blockType = BlockType::Paragraph; result.spans = parseInline(trimmed); diff --git a/lib/Md/MdParser.h b/lib/Md/MdParser.h index a5f68062..a4ba8ce5 100644 --- a/lib/Md/MdParser.h +++ b/lib/Md/MdParser.h @@ -23,10 +23,7 @@ enum class BlockType : uint8_t { Blockquote, CodeBlock, HorizontalRule, - BlankLine, - TableHeader, // | col | col | — header row - TableSeparator, // | --- | --- | — ignored visually - TableRow, // | val | val | — data row + BlankLine }; struct ParsedLine { @@ -34,15 +31,8 @@ struct ParsedLine { std::vector spans; std::string listPrefix; // "• " or "1. " etc. uint8_t indentLevel = 0; // Nesting depth (each 4 spaces = 1 level) - // For TableHeader / TableRow: one entry per cell (already inline-parsed) - std::vector> tableCells; }; -// Returns true if the line is a GFM pipe-table row (| ... |) or separator (| --- |). -bool isTableRow(const std::string& line); -// Returns true if the line is a GFM table separator row (| :---: | --- |). -bool isTableSeparator(const std::string& line); - // Parse a single raw line of markdown into block type and styled spans. // |inCodeBlock| indicates whether the line is inside a fenced code block. ParsedLine parseLine(const std::string& rawLine, bool inCodeBlock); diff --git a/src/activities/reader/MdReaderActivity.cpp b/src/activities/reader/MdReaderActivity.cpp index 3aa172be..6c211221 100644 --- a/src/activities/reader/MdReaderActivity.cpp +++ b/src/activities/reader/MdReaderActivity.cpp @@ -24,7 +24,7 @@ namespace { constexpr size_t CHUNK_SIZE = 8 * 1024; constexpr size_t MAX_LINE_LENGTH = 64 * 1024; constexpr uint32_t CACHE_MAGIC = 0x4D4B4449; // "MKDI" -constexpr uint8_t CACHE_VERSION = 4; // Bumped: GFM pipe table support +constexpr uint8_t CACHE_VERSION = 3; // Bumped: nested list indent + task checkboxes static std::string flattenHeadingText(const MdParser::ParsedLine& parsed) { std::string result; @@ -535,68 +535,6 @@ bool MdReaderActivity::loadPageAtOffset(size_t offset, bool startInCodeBlock, st parsed = MdParser::parseLine(rawLine, inCodeBlock); } - // GFM table handling: separator rows are invisible; the first data row in a table - // block is promoted to TableHeader (bold cells + separator line drawn after it). - if (parsed.blockType == MdParser::BlockType::TableSeparator) { - pos = lineEnd + 1; - continue; - } - if (parsed.blockType == MdParser::BlockType::TableRow) { - // Promote first row of a table to header if immediately followed by a separator. - // We detect this by peeking ahead at the next line in the buffer. - bool isHeader = false; - { - size_t nextLineStart = lineEnd + 1; - size_t nextLineEnd = nextLineStart; - while (nextLineEnd < bufferSize && pageBuffer[nextLineEnd] != '\n') nextLineEnd++; - std::string nextRaw(reinterpret_cast(pageBuffer.data() + nextLineStart), nextLineEnd - nextLineStart); - if (!nextRaw.empty() && nextRaw.back() == '\r') nextRaw.pop_back(); - isHeader = MdParser::isTableSeparator(nextRaw); - } - if (isHeader) parsed.blockType = MdParser::BlockType::TableHeader; - - // Flatten cells into a single line: "Cell1 │ Cell2 │ Cell3" - // Bold all spans when this is a header row. - MdParser::ParsedLine flatLine; - flatLine.blockType = MdParser::BlockType::Paragraph; - bool first = true; - for (auto& cell : parsed.tableCells) { - if (!first) { - flatLine.spans.push_back({" \xe2\x94\x82 ", EpdFontFamily::REGULAR}); // " │ " - } - first = false; - for (auto& span : cell) { - if (isHeader) { - span.style = static_cast(static_cast(span.style) | - static_cast(EpdFontFamily::BOLD)); - } - flatLine.spans.push_back(span); - } - } - - size_t linesBefore = outLines.size(); - int remainingLines = linesPerPage - static_cast(outLines.size()); - bool fullyConsumed = wordWrapParsedLine(flatLine, 0, outLines, remainingLines); - if (!fullyConsumed) { - if (linesBefore > 0) { - outLines.resize(linesBefore); - } else { - pos = lineComplete ? lineEnd + 1 : lineEnd; - } - break; - } - - // After a header row, add a thin separator line (reuse isHR rendering) - if (isHeader && static_cast(outLines.size()) < linesPerPage) { - RenderedLine sep; - sep.isHR = true; - outLines.push_back(sep); - } - - pos = lineEnd + 1; - continue; - } - // Determine indent (base + nesting level) int indent = 0; switch (parsed.blockType) {