diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 87033f0f..095d0546 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -208,30 +208,14 @@ bool Epub::parseTocNavFile() const { return true; } -std::string Epub::getCssRulesCache() const { return cachePath + "/css_rules.cache"; } - -bool Epub::loadCssRulesFromCache() const { - FsFile cssCacheFile; - if (Storage.openFileForRead("EBP", getCssRulesCache(), cssCacheFile)) { - if (cssParser->loadFromCache(cssCacheFile)) { - cssCacheFile.close(); - LOG_DBG("EBP", "Loaded CSS rules from cache"); - return true; - } - cssCacheFile.close(); - LOG_DBG("EBP", "CSS cache invalid, reparsing"); - } - return false; -} - void Epub::parseCssFiles() const { if (cssFiles.empty()) { LOG_DBG("EBP", "No CSS files to parse, but CssParser created for inline styles"); } - // Try to load from CSS cache first - if (!loadCssRulesFromCache()) { - // Cache miss - parse CSS files + // See if we have a cached version of the CSS rules + if (!cssParser->hasCache()) { + // No cache yet - parse CSS files for (const auto& cssPath : cssFiles) { LOG_DBG("EBP", "Parsing CSS file: %s", cssPath.c_str()); @@ -262,11 +246,10 @@ void Epub::parseCssFiles() const { } // Save to cache for next time - FsFile cssCacheFile; - if (Storage.openFileForWrite("EBP", getCssRulesCache(), cssCacheFile)) { - cssParser->saveToCache(cssCacheFile); - cssCacheFile.close(); + if (!cssParser->saveToCache()) { + LOG_ERR("EBP", "Failed to save CSS rules to cache"); } + cssParser->clear(); LOG_DBG("EBP", "Loaded %zu CSS style rules from %zu files", cssParser->ruleCount(), cssFiles.size()); } @@ -279,11 +262,11 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) { // Initialize spine/TOC cache bookMetadataCache.reset(new BookMetadataCache(cachePath)); // Always create CssParser - needed for inline style parsing even without CSS files - cssParser.reset(new CssParser()); + cssParser.reset(new CssParser(cachePath)); // Try to load existing cache first if (bookMetadataCache->load()) { - if (!skipLoadingCss && !loadCssRulesFromCache()) { + if (!skipLoadingCss && !cssParser->hasCache()) { LOG_DBG("EBP", "Warning: CSS rules cache not found, attempting to parse CSS files"); // to get CSS file list if (!parseContentOpf(bookMetadataCache->coreMetadata)) { diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index 0efde410..cde9d210 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -35,8 +35,6 @@ class Epub { bool parseTocNcxFile() const; bool parseTocNavFile() const; void parseCssFiles() const; - std::string getCssRulesCache() const; - bool loadCssRulesFromCache() const; public: explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) { @@ -73,5 +71,5 @@ class Epub { size_t getBookSize() const; float calculateProgress(int currentSpineIndex, float currentSpineRead) const; - const CssParser* getCssParser() const { return cssParser.get(); } + CssParser* getCssParser() const { return cssParser.get(); } }; diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 5039c57b..496bdb5c 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -181,11 +181,20 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c viewportHeight, hyphenationEnabled, embeddedStyle); std::vector lut = {}; + CssParser* cssParser = nullptr; + if (embeddedStyle) { + cssParser = epub->getCssParser(); + if (cssParser) { + if (!cssParser->loadFromCache()) { + LOG_ERR("SCT", "Failed to load CSS from cache"); + } + } + } ChapterHtmlSlimParser visitor( tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, viewportHeight, hyphenationEnabled, [this, &lut](std::unique_ptr page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, - embeddedStyle, popupFn, embeddedStyle ? epub->getCssParser() : nullptr); + embeddedStyle, popupFn, cssParser); Hyphenator::setPreferredLanguage(epub->getLanguage()); success = visitor.parseAndBuildPages(); @@ -194,6 +203,9 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c LOG_ERR("SCT", "Failed to parse XML and build pages"); file.close(); Storage.remove(filePath.c_str()); + if (cssParser) { + cssParser->clear(); + } return false; } @@ -220,6 +232,9 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c serialization::writePod(file, pageCount); serialization::writePod(file, lutOffset); file.close(); + if (cssParser) { + cssParser->clear(); + } return true; } diff --git a/lib/Epub/Epub/css/CssParser.cpp b/lib/Epub/Epub/css/CssParser.cpp index 0a11e80e..590b2215 100644 --- a/lib/Epub/Epub/css/CssParser.cpp +++ b/lib/Epub/Epub/css/CssParser.cpp @@ -1,144 +1,57 @@ #include "CssParser.h" +#include #include #include +#include #include +#include namespace { +// Stack-allocated string buffer to avoid heap reallocations during parsing +// Provides string-like interface with fixed capacity +struct StackBuffer { + static constexpr size_t CAPACITY = 1024; + char data[CAPACITY]; + size_t len = 0; + + void push_back(char c) { + if (len < CAPACITY - 1) { + data[len++] = c; + } + } + + void clear() { len = 0; } + bool empty() const { return len == 0; } + size_t size() const { return len; } + + // Get string view of current content (zero-copy) + std::string_view view() const { return std::string_view(data, len); } + + // Convert to string for passing to functions (single allocation) + std::string str() const { return std::string(data, len); } +}; + // Buffer size for reading CSS files constexpr size_t READ_BUFFER_SIZE = 512; -// Maximum CSS file size we'll process (prevent memory issues) -constexpr size_t MAX_CSS_SIZE = 64 * 1024; +// Maximum number of CSS rules to store in the selector map +// Prevents unbounded memory growth from pathological CSS files +constexpr size_t MAX_RULES = 1500; + +// Minimum free heap required to apply CSS during rendering +// If below this threshold, we skip CSS to avoid display artifacts. +constexpr size_t MIN_FREE_HEAP_FOR_CSS = 48 * 1024; + +// Maximum length for a single selector string +// Prevents parsing of extremely long or malformed selectors +constexpr size_t MAX_SELECTOR_LENGTH = 256; // Check if character is CSS whitespace bool isCssWhitespace(const char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; } -// Read entire file into string (with size limit) -std::string readFileContent(FsFile& file) { - std::string content; - content.reserve(std::min(static_cast(file.size()), MAX_CSS_SIZE)); - - char buffer[READ_BUFFER_SIZE]; - while (file.available() && content.size() < MAX_CSS_SIZE) { - const int bytesRead = file.read(buffer, sizeof(buffer)); - if (bytesRead <= 0) break; - content.append(buffer, bytesRead); - } - return content; -} - -// Remove CSS comments (/* ... */) from content -std::string stripComments(const std::string& css) { - std::string result; - result.reserve(css.size()); - - size_t pos = 0; - while (pos < css.size()) { - // Look for start of comment - if (pos + 1 < css.size() && css[pos] == '/' && css[pos + 1] == '*') { - // Find end of comment - const size_t endPos = css.find("*/", pos + 2); - if (endPos == std::string::npos) { - // Unterminated comment - skip rest of file - break; - } - pos = endPos + 2; - } else { - result.push_back(css[pos]); - ++pos; - } - } - return result; -} - -// Skip @-rules (like @media, @import, @font-face) -// Returns position after the @-rule -size_t skipAtRule(const std::string& css, const size_t start) { - // Find the end - either semicolon (simple @-rule) or matching brace - size_t pos = start + 1; // Skip the '@' - - // Skip identifier - while (pos < css.size() && (std::isalnum(css[pos]) || css[pos] == '-')) { - ++pos; - } - - // Look for { or ; - int braceDepth = 0; - while (pos < css.size()) { - const char c = css[pos]; - if (c == '{') { - ++braceDepth; - } else if (c == '}') { - --braceDepth; - if (braceDepth == 0) { - return pos + 1; - } - } else if (c == ';' && braceDepth == 0) { - return pos + 1; - } - ++pos; - } - return css.size(); -} - -// Extract next rule from CSS content -// Returns true if a rule was found, with selector and body filled -bool extractNextRule(const std::string& css, size_t& pos, std::string& selector, std::string& body) { - selector.clear(); - body.clear(); - - // Skip whitespace and @-rules until we find a regular rule - while (pos < css.size()) { - // Skip whitespace - while (pos < css.size() && isCssWhitespace(css[pos])) { - ++pos; - } - - if (pos >= css.size()) return false; - - // Handle @-rules iteratively (avoids recursion/stack overflow) - if (css[pos] == '@') { - pos = skipAtRule(css, pos); - continue; // Try again after skipping the @-rule - } - - break; // Found start of a regular rule - } - - if (pos >= css.size()) return false; - - // Find opening brace - const size_t bracePos = css.find('{', pos); - if (bracePos == std::string::npos) return false; - - // Extract selector (everything before the brace) - selector = css.substr(pos, bracePos - pos); - - // Find matching closing brace - int depth = 1; - const size_t bodyStart = bracePos + 1; - size_t bodyEnd = bodyStart; - - while (bodyEnd < css.size() && depth > 0) { - if (css[bodyEnd] == '{') - ++depth; - else if (css[bodyEnd] == '}') - --depth; - ++bodyEnd; - } - - // Extract body (between braces) - if (bodyEnd > bodyStart) { - body = css.substr(bodyStart, bodyEnd - bodyStart - 1); - } - - pos = bodyEnd; - return true; -} - } // anonymous namespace // String utilities implementation @@ -167,6 +80,28 @@ std::string CssParser::normalized(const std::string& s) { return result; } +void CssParser::normalizedInto(const std::string& s, std::string& out) { + out.clear(); + out.reserve(s.size()); + + bool inSpace = true; // Start true to skip leading space + for (const char c : s) { + if (isCssWhitespace(c)) { + if (!inSpace) { + out.push_back(' '); + inSpace = true; + } + } else { + out.push_back(static_cast(std::tolower(static_cast(c)))); + inSpace = false; + } + } + + if (!out.empty() && out.back() == ' ') { + out.pop_back(); + } +} + std::vector CssParser::splitOnChar(const std::string& s, const char delimiter) { std::vector parts; size_t start = 0; @@ -290,129 +225,95 @@ CssLength CssParser::interpretLength(const std::string& val) { return CssLength{numericValue, unit}; } +// Declaration parsing -int8_t CssParser::interpretSpacing(const std::string& val) { - const std::string v = normalized(val); - if (v.empty()) return 0; +void CssParser::parseDeclarationIntoStyle(const std::string& decl, CssStyle& style, std::string& propNameBuf, + std::string& propValueBuf) { + const size_t colonPos = decl.find(':'); + if (colonPos == std::string::npos || colonPos == 0) return; - // For spacing, we convert to "lines" (discrete units for e-ink) - // 1em ≈ 1 line, percentages based on ~30 lines per page + normalizedInto(decl.substr(0, colonPos), propNameBuf); + normalizedInto(decl.substr(colonPos + 1), propValueBuf); - float multiplier = 0.0f; - size_t unitStart = v.size(); + if (propNameBuf.empty() || propValueBuf.empty()) return; - for (size_t i = 0; i < v.size(); ++i) { - const char c = v[i]; - if (!std::isdigit(c) && c != '.' && c != '-' && c != '+') { - unitStart = i; - break; + if (propNameBuf == "text-align") { + style.textAlign = interpretAlignment(propValueBuf); + style.defined.textAlign = 1; + } else if (propNameBuf == "font-style") { + style.fontStyle = interpretFontStyle(propValueBuf); + style.defined.fontStyle = 1; + } else if (propNameBuf == "font-weight") { + style.fontWeight = interpretFontWeight(propValueBuf); + style.defined.fontWeight = 1; + } else if (propNameBuf == "text-decoration" || propNameBuf == "text-decoration-line") { + style.textDecoration = interpretDecoration(propValueBuf); + style.defined.textDecoration = 1; + } else if (propNameBuf == "text-indent") { + style.textIndent = interpretLength(propValueBuf); + style.defined.textIndent = 1; + } else if (propNameBuf == "margin-top") { + style.marginTop = interpretLength(propValueBuf); + style.defined.marginTop = 1; + } else if (propNameBuf == "margin-bottom") { + style.marginBottom = interpretLength(propValueBuf); + style.defined.marginBottom = 1; + } else if (propNameBuf == "margin-left") { + style.marginLeft = interpretLength(propValueBuf); + style.defined.marginLeft = 1; + } else if (propNameBuf == "margin-right") { + style.marginRight = interpretLength(propValueBuf); + style.defined.marginRight = 1; + } else if (propNameBuf == "margin") { + const auto values = splitWhitespace(propValueBuf); + if (!values.empty()) { + style.marginTop = interpretLength(values[0]); + style.marginRight = values.size() >= 2 ? interpretLength(values[1]) : style.marginTop; + style.marginBottom = values.size() >= 3 ? interpretLength(values[2]) : style.marginTop; + style.marginLeft = values.size() >= 4 ? interpretLength(values[3]) : style.marginRight; + style.defined.marginTop = style.defined.marginRight = style.defined.marginBottom = style.defined.marginLeft = 1; + } + } else if (propNameBuf == "padding-top") { + style.paddingTop = interpretLength(propValueBuf); + style.defined.paddingTop = 1; + } else if (propNameBuf == "padding-bottom") { + style.paddingBottom = interpretLength(propValueBuf); + style.defined.paddingBottom = 1; + } else if (propNameBuf == "padding-left") { + style.paddingLeft = interpretLength(propValueBuf); + style.defined.paddingLeft = 1; + } else if (propNameBuf == "padding-right") { + style.paddingRight = interpretLength(propValueBuf); + style.defined.paddingRight = 1; + } else if (propNameBuf == "padding") { + const auto values = splitWhitespace(propValueBuf); + if (!values.empty()) { + style.paddingTop = interpretLength(values[0]); + style.paddingRight = values.size() >= 2 ? interpretLength(values[1]) : style.paddingTop; + style.paddingBottom = values.size() >= 3 ? interpretLength(values[2]) : style.paddingTop; + style.paddingLeft = values.size() >= 4 ? interpretLength(values[3]) : style.paddingRight; + style.defined.paddingTop = style.defined.paddingRight = style.defined.paddingBottom = style.defined.paddingLeft = + 1; } } - - const std::string numPart = v.substr(0, unitStart); - const std::string unitPart = v.substr(unitStart); - - if (unitPart == "em" || unitPart == "rem") { - multiplier = 1.0f; // 1em = 1 line - } else if (unitPart == "%") { - multiplier = 0.3f; // ~30 lines per page, so 10% = 3 lines - } else { - return 0; // Unsupported unit for spacing - } - - char* endPtr = nullptr; - const float numericValue = std::strtof(numPart.c_str(), &endPtr); - - if (endPtr == numPart.c_str()) return 0; - - int lines = static_cast(numericValue * multiplier); - - // Clamp to reasonable range (0-2 lines) - if (lines < 0) lines = 0; - if (lines > 2) lines = 2; - - return static_cast(lines); } -// Declaration parsing - CssStyle CssParser::parseDeclarations(const std::string& declBlock) { CssStyle style; + std::string propNameBuf; + std::string propValueBuf; - // Split declarations by semicolon - const auto declarations = splitOnChar(declBlock, ';'); - - for (const auto& decl : declarations) { - // Find colon separator - const size_t colonPos = decl.find(':'); - if (colonPos == std::string::npos || colonPos == 0) continue; - - std::string propName = normalized(decl.substr(0, colonPos)); - std::string propValue = normalized(decl.substr(colonPos + 1)); - - if (propName.empty() || propValue.empty()) continue; - - // Match property and set value - if (propName == "text-align") { - style.textAlign = interpretAlignment(propValue); - style.defined.textAlign = 1; - } else if (propName == "font-style") { - style.fontStyle = interpretFontStyle(propValue); - style.defined.fontStyle = 1; - } else if (propName == "font-weight") { - style.fontWeight = interpretFontWeight(propValue); - style.defined.fontWeight = 1; - } else if (propName == "text-decoration" || propName == "text-decoration-line") { - style.textDecoration = interpretDecoration(propValue); - style.defined.textDecoration = 1; - } else if (propName == "text-indent") { - style.textIndent = interpretLength(propValue); - style.defined.textIndent = 1; - } else if (propName == "margin-top") { - style.marginTop = interpretLength(propValue); - style.defined.marginTop = 1; - } else if (propName == "margin-bottom") { - style.marginBottom = interpretLength(propValue); - style.defined.marginBottom = 1; - } else if (propName == "margin-left") { - style.marginLeft = interpretLength(propValue); - style.defined.marginLeft = 1; - } else if (propName == "margin-right") { - style.marginRight = interpretLength(propValue); - style.defined.marginRight = 1; - } else if (propName == "margin") { - // Shorthand: 1-4 values for top, right, bottom, left - const auto values = splitWhitespace(propValue); - if (!values.empty()) { - style.marginTop = interpretLength(values[0]); - style.marginRight = values.size() >= 2 ? interpretLength(values[1]) : style.marginTop; - style.marginBottom = values.size() >= 3 ? interpretLength(values[2]) : style.marginTop; - style.marginLeft = values.size() >= 4 ? interpretLength(values[3]) : style.marginRight; - style.defined.marginTop = style.defined.marginRight = style.defined.marginBottom = style.defined.marginLeft = 1; - } - } else if (propName == "padding-top") { - style.paddingTop = interpretLength(propValue); - style.defined.paddingTop = 1; - } else if (propName == "padding-bottom") { - style.paddingBottom = interpretLength(propValue); - style.defined.paddingBottom = 1; - } else if (propName == "padding-left") { - style.paddingLeft = interpretLength(propValue); - style.defined.paddingLeft = 1; - } else if (propName == "padding-right") { - style.paddingRight = interpretLength(propValue); - style.defined.paddingRight = 1; - } else if (propName == "padding") { - // Shorthand: 1-4 values for top, right, bottom, left - const auto values = splitWhitespace(propValue); - if (!values.empty()) { - style.paddingTop = interpretLength(values[0]); - style.paddingRight = values.size() >= 2 ? interpretLength(values[1]) : style.paddingTop; - style.paddingBottom = values.size() >= 3 ? interpretLength(values[2]) : style.paddingTop; - style.paddingLeft = values.size() >= 4 ? interpretLength(values[3]) : style.paddingRight; - style.defined.paddingTop = style.defined.paddingRight = style.defined.paddingBottom = - style.defined.paddingLeft = 1; + size_t start = 0; + for (size_t i = 0; i <= declBlock.size(); ++i) { + if (i == declBlock.size() || declBlock[i] == ';') { + if (i > start) { + const size_t len = i - start; + std::string decl = declBlock.substr(start, len); + if (!decl.empty()) { + parseDeclarationIntoStyle(decl, style, propNameBuf, propValueBuf); + } } + start = i + 1; } } @@ -421,20 +322,33 @@ CssStyle CssParser::parseDeclarations(const std::string& declBlock) { // Rule processing -void CssParser::processRuleBlock(const std::string& selectorGroup, const std::string& declarations) { - const CssStyle style = parseDeclarations(declarations); - - // Only store if any properties were set - if (!style.defined.anySet()) return; +void CssParser::processRuleBlockWithStyle(const std::string& selectorGroup, const CssStyle& style) { + // Check if we've reached the rule limit before processing + if (rulesBySelector_.size() >= MAX_RULES) { + LOG_DBG("CSS", "Reached max rules limit (%zu), stopping CSS parsing", MAX_RULES); + return; + } // Handle comma-separated selectors const auto selectors = splitOnChar(selectorGroup, ','); for (const auto& sel : selectors) { + // Validate selector length before processing + if (sel.size() > MAX_SELECTOR_LENGTH) { + LOG_DBG("CSS", "Selector too long (%zu > %zu), skipping", sel.size(), MAX_SELECTOR_LENGTH); + continue; + } + // Normalize the selector std::string key = normalized(sel); if (key.empty()) continue; + // Skip if this would exceed the rule limit + if (rulesBySelector_.size() >= MAX_RULES) { + LOG_DBG("CSS", "Reached max rules limit, stopping selector processing"); + return; + } + // Store or merge with existing auto it = rulesBySelector_.find(key); if (it != rulesBySelector_.end()) { @@ -453,30 +367,158 @@ bool CssParser::loadFromStream(FsFile& source) { return false; } - // Read file content - const std::string content = readFileContent(source); - if (content.empty()) { - return true; // Empty file is valid + size_t totalRead = 0; + + // Use stack-allocated buffers for parsing to avoid heap reallocations + StackBuffer selector; + StackBuffer declBuffer; + // Keep these as std::string since they're passed by reference to parseDeclarationIntoStyle + std::string propNameBuf; + std::string propValueBuf; + + bool inComment = false; + bool maybeSlash = false; + bool prevStar = false; + + bool inAtRule = false; + int atDepth = 0; + + int bodyDepth = 0; + bool skippingRule = false; + CssStyle currentStyle; + + auto handleChar = [&](const char c) { + if (inAtRule) { + if (c == '{') { + ++atDepth; + } else if (c == '}') { + if (atDepth > 0) --atDepth; + if (atDepth == 0) inAtRule = false; + } else if (c == ';' && atDepth == 0) { + inAtRule = false; + } + return; + } + + if (bodyDepth == 0) { + if (selector.empty() && isCssWhitespace(c)) { + return; + } + if (c == '@' && selector.empty()) { + inAtRule = true; + atDepth = 0; + return; + } + if (c == '{') { + bodyDepth = 1; + currentStyle = CssStyle{}; + declBuffer.clear(); + if (selector.size() > MAX_SELECTOR_LENGTH * 4) { + skippingRule = true; + } + return; + } + selector.push_back(c); + return; + } + + // bodyDepth > 0 + if (c == '{') { + ++bodyDepth; + return; + } + if (c == '}') { + --bodyDepth; + if (bodyDepth == 0) { + if (!skippingRule && !declBuffer.empty()) { + parseDeclarationIntoStyle(declBuffer.str(), currentStyle, propNameBuf, propValueBuf); + } + if (!skippingRule) { + processRuleBlockWithStyle(selector.str(), currentStyle); + } + selector.clear(); + declBuffer.clear(); + skippingRule = false; + return; + } + return; + } + if (bodyDepth > 1) { + return; + } + if (!skippingRule) { + if (c == ';') { + if (!declBuffer.empty()) { + parseDeclarationIntoStyle(declBuffer.str(), currentStyle, propNameBuf, propValueBuf); + declBuffer.clear(); + } + } else { + declBuffer.push_back(c); + } + } + }; + + char buffer[READ_BUFFER_SIZE]; + while (source.available()) { + int bytesRead = source.read(buffer, sizeof(buffer)); + if (bytesRead <= 0) break; + + totalRead += static_cast(bytesRead); + + for (int i = 0; i < bytesRead; ++i) { + const char c = buffer[i]; + + if (inComment) { + if (prevStar && c == '/') { + inComment = false; + prevStar = false; + continue; + } + prevStar = c == '*'; + continue; + } + + if (maybeSlash) { + if (c == '*') { + inComment = true; + maybeSlash = false; + prevStar = false; + continue; + } + handleChar('/'); + maybeSlash = false; + // fall through to process current char + } + + if (c == '/') { + maybeSlash = true; + continue; + } + + handleChar(c); + } } - // Remove comments - const std::string cleaned = stripComments(content); - - // Parse rules - size_t pos = 0; - std::string selector, body; - - while (extractNextRule(cleaned, pos, selector, body)) { - processRuleBlock(selector, body); + if (maybeSlash) { + handleChar('/'); } - LOG_DBG("CSS", "Parsed %zu rules", rulesBySelector_.size()); + LOG_DBG("CSS", "Parsed %zu rules from %zu bytes", rulesBySelector_.size(), totalRead); return true; } // Style resolution CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string& classAttr) const { + static bool lowHeapWarningLogged = false; + if (ESP.getFreeHeap() < MIN_FREE_HEAP_FOR_CSS) { + if (!lowHeapWarningLogged) { + lowHeapWarningLogged = true; + LOG_DBG("CSS", "Warning: low heap (%u bytes) below MIN_FREE_HEAP_FOR_CSS (%u), returning empty style", + ESP.getFreeHeap(), static_cast(MIN_FREE_HEAP_FOR_CSS)); + } + return CssStyle{}; + } CssStyle result; const std::string tag = normalized(tagName); @@ -521,9 +563,17 @@ CssStyle CssParser::parseInlineStyle(const std::string& styleValue) { return par // Cache format version - increment when format changes constexpr uint8_t CSS_CACHE_VERSION = 2; +constexpr char rulesCache[] = "/css_rules.cache"; -bool CssParser::saveToCache(FsFile& file) const { - if (!file) { +bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); } + +bool CssParser::saveToCache() const { + if (cachePath.empty()) { + return false; + } + + FsFile file; + if (!Storage.openFileForWrite("CSS", cachePath + rulesCache, file)) { return false; } @@ -583,11 +633,17 @@ bool CssParser::saveToCache(FsFile& file) const { } LOG_DBG("CSS", "Saved %u rules to cache", ruleCount); + file.close(); return true; } -bool CssParser::loadFromCache(FsFile& file) { - if (!file) { +bool CssParser::loadFromCache() { + if (cachePath.empty()) { + return false; + } + + FsFile file; + if (!Storage.openFileForRead("CSS", cachePath + rulesCache, file)) { return false; } @@ -598,12 +654,14 @@ bool CssParser::loadFromCache(FsFile& file) { uint8_t version = 0; if (file.read(&version, 1) != 1 || version != CSS_CACHE_VERSION) { LOG_DBG("CSS", "Cache version mismatch (got %u, expected %u)", version, CSS_CACHE_VERSION); + file.close(); return false; } // Read rule count uint16_t ruleCount = 0; if (file.read(&ruleCount, sizeof(ruleCount)) != sizeof(ruleCount)) { + file.close(); return false; } @@ -613,6 +671,7 @@ bool CssParser::loadFromCache(FsFile& file) { uint16_t selectorLen = 0; if (file.read(&selectorLen, sizeof(selectorLen)) != sizeof(selectorLen)) { rulesBySelector_.clear(); + file.close(); return false; } @@ -620,6 +679,7 @@ bool CssParser::loadFromCache(FsFile& file) { selector.resize(selectorLen); if (file.read(&selector[0], selectorLen) != selectorLen) { rulesBySelector_.clear(); + file.close(); return false; } @@ -629,24 +689,28 @@ bool CssParser::loadFromCache(FsFile& file) { if (file.read(&enumVal, 1) != 1) { rulesBySelector_.clear(); + file.close(); return false; } style.textAlign = static_cast(enumVal); if (file.read(&enumVal, 1) != 1) { rulesBySelector_.clear(); + file.close(); return false; } style.fontStyle = static_cast(enumVal); if (file.read(&enumVal, 1) != 1) { rulesBySelector_.clear(); + file.close(); return false; } style.fontWeight = static_cast(enumVal); if (file.read(&enumVal, 1) != 1) { rulesBySelector_.clear(); + file.close(); return false; } style.textDecoration = static_cast(enumVal); @@ -668,6 +732,7 @@ bool CssParser::loadFromCache(FsFile& file) { !readLength(style.marginLeft) || !readLength(style.marginRight) || !readLength(style.paddingTop) || !readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight)) { rulesBySelector_.clear(); + file.close(); return false; } @@ -675,6 +740,7 @@ bool CssParser::loadFromCache(FsFile& file) { uint16_t definedBits = 0; if (file.read(&definedBits, sizeof(definedBits)) != sizeof(definedBits)) { rulesBySelector_.clear(); + file.close(); return false; } style.defined.textAlign = (definedBits & 1 << 0) != 0; @@ -695,5 +761,6 @@ bool CssParser::loadFromCache(FsFile& file) { } LOG_DBG("CSS", "Loaded %u rules from cache", ruleCount); + file.close(); return true; } diff --git a/lib/Epub/Epub/css/CssParser.h b/lib/Epub/Epub/css/CssParser.h index 5605553d..60f70d23 100644 --- a/lib/Epub/Epub/css/CssParser.h +++ b/lib/Epub/Epub/css/CssParser.h @@ -4,6 +4,7 @@ #include #include +#include #include #include "CssStyle.h" @@ -29,7 +30,7 @@ */ class CssParser { public: - CssParser() = default; + explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {} ~CssParser() = default; // Non-copyable @@ -76,28 +77,35 @@ class CssParser { */ void clear() { rulesBySelector_.clear(); } + /** + * Check if CSS rules cache file exists + */ + bool hasCache() const; + /** * Save parsed CSS rules to a cache file. - * @param file Open file handle to write to * @return true if cache was written successfully */ - bool saveToCache(FsFile& file) const; + bool saveToCache() const; /** * Load CSS rules from a cache file. * Clears any existing rules before loading. - * @param file Open file handle to read from * @return true if cache was loaded successfully */ - bool loadFromCache(FsFile& file); + bool loadFromCache(); private: // Storage: maps normalized selector -> style properties std::unordered_map rulesBySelector_; + std::string cachePath; + // Internal parsing helpers - void processRuleBlock(const std::string& selectorGroup, const std::string& declarations); + void processRuleBlockWithStyle(const std::string& selectorGroup, const CssStyle& style); static CssStyle parseDeclarations(const std::string& declBlock); + static void parseDeclarationIntoStyle(const std::string& decl, CssStyle& style, std::string& propNameBuf, + std::string& propValueBuf); // Individual property value parsers static CssTextAlign interpretAlignment(const std::string& val); @@ -105,10 +113,10 @@ class CssParser { static CssFontWeight interpretFontWeight(const std::string& val); static CssTextDecoration interpretDecoration(const std::string& val); static CssLength interpretLength(const std::string& val); - static int8_t interpretSpacing(const std::string& val); // String utilities static std::string normalized(const std::string& s); + static void normalizedInto(const std::string& s, std::string& out); static std::vector splitOnChar(const std::string& s, char delimiter); static std::vector splitWhitespace(const std::string& s); }; diff --git a/platformio.ini b/platformio.ini index 281eab17..20383f4f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -27,10 +27,13 @@ build_flags = # https://libexpat.github.io/doc/api/latest/#XML_GE -DXML_GE=0 -DXML_CONTEXT_BYTES=1024 - -std=c++2a + -std=gnu++2a # Enable UTF-8 long file names in SdFat -DUSE_UTF8_LONG_NAMES=1 +build_unflags = + -std=gnu++11 + ; Board configuration board_build.flash_mode = dio board_build.flash_size = 16MB diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 25092d85..1306c217 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -88,7 +88,7 @@ RecentBook RecentBooksStore::getDataFromBook(std::string path) const { // If epub, try to load the metadata for title/author and cover if (StringUtils::checkFileExtension(lastBookFileName, ".epub")) { Epub epub(path, "/.crosspoint"); - epub.load(false); + epub.load(false, true); return RecentBook{path, epub.getTitle(), epub.getAuthor(), epub.getThumbBmpPath()}; } else if (StringUtils::checkFileExtension(lastBookFileName, ".xtch") || StringUtils::checkFileExtension(lastBookFileName, ".xtc")) { diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index b336c9a3..f2f9199a 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -2,6 +2,7 @@ #include +#include "CrossPointSettings.h" #include "Epub.h" #include "EpubReaderActivity.h" #include "Txt.h" @@ -35,7 +36,7 @@ std::unique_ptr ReaderActivity::loadEpub(const std::string& path) { } auto epub = std::unique_ptr(new Epub(path, "/.crosspoint")); - if (epub->load()) { + if (epub->load(true, SETTINGS.embeddedStyle == 0)) { return epub; }