Revert "Adapt table implementation from crossink"
This reverts commit ca9475f0c3.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
#include "Page.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
@@ -49,135 +48,6 @@ std::unique_ptr<PageImage> PageImage::deserialize(FsFile& file) {
|
||||
return std::unique_ptr<PageImage>(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<uint8_t>(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<uint16_t>(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<uint8_t>(row.cells.size());
|
||||
serialization::writePod(file, cellCount);
|
||||
for (const auto& cell : row.cells) {
|
||||
serialization::writePod(file, cell.isHeader);
|
||||
const uint8_t lineCount = static_cast<uint8_t>(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> 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<uint16_t, MAX_TABLE_COLS> 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<TableRow> 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<PageTableFragment>(
|
||||
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> 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;
|
||||
|
||||
+1
-45
@@ -2,7 +2,6 @@
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -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<std::shared_ptr<TextBlock>> lines;
|
||||
bool isHeader = false;
|
||||
};
|
||||
|
||||
struct TableRow {
|
||||
std::vector<TableCell> 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<uint16_t, MAX_TABLE_COLS> colWidths = {};
|
||||
std::vector<TableRow> rows;
|
||||
|
||||
public:
|
||||
PageTableFragment(uint8_t colCount, uint16_t totalWidth, uint16_t totalHeight,
|
||||
std::array<uint16_t, MAX_TABLE_COLS> colWidths, std::vector<TableRow> 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<PageTableFragment> 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -188,29 +188,25 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::STRIKETHROUGH);
|
||||
}
|
||||
|
||||
// flush the buffer — route to table cell text when inside a <td>/<th>
|
||||
// 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<uint16_t>(viewportWidth - horizontalInset) : viewportWidth;
|
||||
currentTextBlock->layoutAndExtractLines(
|
||||
renderer, fontId, effectiveWidth,
|
||||
[this](const std::shared_ptr<TextBlock>& 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<uint16_t>(viewportWidth - horizontalInset) : viewportWidth;
|
||||
currentTextBlock->layoutAndExtractLines(
|
||||
renderer, fontId, effectiveWidth,
|
||||
[this](const std::shared_ptr<TextBlock>& 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 </table>.
|
||||
// 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<BufferedTable>(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<uint8_t>(CssTextAlign::None))
|
||||
? CssTextAlign::Justify
|
||||
: static_cast<CssTextAlign>(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<int>(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<ParsedText>(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<ChapterHtmlSlimParser*>(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<uint8_t>(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<uint16_t>(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<uint16_t, MAX_TABLE_COLS> colWidths = {};
|
||||
for (uint8_t c = 0; c < columnCount; c++) {
|
||||
colWidths[c] = colWidth;
|
||||
}
|
||||
|
||||
const int lineHeight = static_cast<int>(renderer.getLineHeight(fontId) * lineCompression + 0.5f);
|
||||
|
||||
// Pre-wrap all cells and compute row heights
|
||||
struct LayoutRow {
|
||||
std::vector<TableCell> cells;
|
||||
uint16_t height = 0;
|
||||
bool isHeaderRow = false;
|
||||
};
|
||||
std::vector<LayoutRow> 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<TextBlock>& 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<uint16_t>(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<uint16_t>(maxLines * lineHeight + 2 * TABLE_CELL_PADDING);
|
||||
if (lr.height == 0) lr.height = static_cast<uint16_t>(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<TableRow> 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<uint16_t>(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<PageTableFragment>(columnCount, totalWidth, fragTotalHeight, colWidths,
|
||||
std::move(fragmentRows),
|
||||
/*xPos=*/0, /*yPos=*/static_cast<int16_t>(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<ParsedText>(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<uint16_t>(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<uint8_t>(CssTextAlign::None))
|
||||
? CssTextAlign::Justify
|
||||
: static_cast<CssTextAlign>(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<TextBlock>& tb, bool lineEndsWithHyphen, bool suppressRetry) {
|
||||
return addLineToPage(tb, lineEndsWithHyphen, suppressRetry);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,22 +73,9 @@ class ChapterHtmlSlimParser final : public Print {
|
||||
bool effectiveItalic = false;
|
||||
bool effectiveUnderline = false;
|
||||
bool effectiveStrikethrough = 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>
|
||||
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.
|
||||
|
||||
@@ -234,47 +234,6 @@ std::vector<Span> parseInline(const std::string& text) {
|
||||
return spans;
|
||||
}
|
||||
|
||||
// Split a pipe-table row into trimmed cell strings.
|
||||
// Input: "| foo | **bar** |" → ["foo", "**bar**"]
|
||||
static std::vector<std::string> splitTableCells(const std::string& line) {
|
||||
std::vector<std::string> 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);
|
||||
|
||||
+1
-11
@@ -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<Span> 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<std::vector<Span>> 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);
|
||||
|
||||
Reference in New Issue
Block a user