From 6115bf3cd22f85dec39e38e0014c0a7622e35da8 Mon Sep 17 00:00:00 2001 From: Jake Kenneally Date: Tue, 3 Feb 2026 19:44:30 -0500 Subject: [PATCH] feat: add CSS rules caching to CssParser Add saveToCache() and loadFromCache() methods to CssParser for persisting parsed CSS rules to disk. The cache format includes: - Version byte for cache invalidation - Rule count - For each rule: length-prefixed selector string + CssStyle fields This allows skipping CSS file parsing on subsequent book opens by loading pre-parsed rules from cache. --- lib/Epub/Epub/css/CssParser.cpp | 181 ++++++++++++++++++++++++++++++++ lib/Epub/Epub/css/CssParser.h | 15 +++ 2 files changed, 196 insertions(+) diff --git a/lib/Epub/Epub/css/CssParser.cpp b/lib/Epub/Epub/css/CssParser.cpp index 2ccaafe9..d51ebba7 100644 --- a/lib/Epub/Epub/css/CssParser.cpp +++ b/lib/Epub/Epub/css/CssParser.cpp @@ -514,3 +514,184 @@ CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string& // Inline style parsing (static - doesn't need rule database) CssStyle CssParser::parseInlineStyle(const std::string& styleValue) { return parseDeclarations(styleValue); } + +// Cache serialization + +// Cache format version - increment when format changes +constexpr uint8_t CSS_CACHE_VERSION = 1; + +bool CssParser::saveToCache(FsFile& file) const { + if (!file) { + return false; + } + + // Write version + file.write(CSS_CACHE_VERSION); + + // Write rule count + const auto ruleCount = static_cast(rulesBySelector_.size()); + file.write(reinterpret_cast(&ruleCount), sizeof(ruleCount)); + + // Write each rule: selector string + CssStyle fields + for (const auto& pair : rulesBySelector_) { + // Write selector string (length-prefixed) + const auto selectorLen = static_cast(pair.first.size()); + file.write(reinterpret_cast(&selectorLen), sizeof(selectorLen)); + file.write(reinterpret_cast(pair.first.data()), selectorLen); + + // Write CssStyle fields (all are POD types) + const CssStyle& style = pair.second; + file.write(static_cast(style.textAlign)); + file.write(static_cast(style.fontStyle)); + file.write(static_cast(style.fontWeight)); + file.write(static_cast(style.textDecoration)); + + // Write CssLength fields (value + unit) + auto writeLength = [&file](const CssLength& len) { + file.write(reinterpret_cast(&len.value), sizeof(len.value)); + file.write(static_cast(len.unit)); + }; + + writeLength(style.textIndent); + writeLength(style.marginTop); + writeLength(style.marginBottom); + writeLength(style.marginLeft); + writeLength(style.marginRight); + writeLength(style.paddingTop); + writeLength(style.paddingBottom); + writeLength(style.paddingLeft); + writeLength(style.paddingRight); + + // Write defined flags as uint16_t + uint16_t definedBits = 0; + if (style.defined.textAlign) definedBits |= 1 << 0; + if (style.defined.fontStyle) definedBits |= 1 << 1; + if (style.defined.fontWeight) definedBits |= 1 << 2; + if (style.defined.textDecoration) definedBits |= 1 << 3; + if (style.defined.textIndent) definedBits |= 1 << 4; + if (style.defined.marginTop) definedBits |= 1 << 5; + if (style.defined.marginBottom) definedBits |= 1 << 6; + if (style.defined.marginLeft) definedBits |= 1 << 7; + if (style.defined.marginRight) definedBits |= 1 << 8; + if (style.defined.paddingTop) definedBits |= 1 << 9; + if (style.defined.paddingBottom) definedBits |= 1 << 10; + if (style.defined.paddingLeft) definedBits |= 1 << 11; + if (style.defined.paddingRight) definedBits |= 1 << 12; + file.write(reinterpret_cast(&definedBits), sizeof(definedBits)); + } + + Serial.printf("[%lu] [CSS] Saved %u rules to cache\n", millis(), ruleCount); + return true; +} + +bool CssParser::loadFromCache(FsFile& file) { + if (!file) { + return false; + } + + // Clear existing rules + clear(); + + // Read and verify version + uint8_t version = 0; + if (file.read(&version, 1) != 1 || version != CSS_CACHE_VERSION) { + Serial.printf("[%lu] [CSS] Cache version mismatch (got %u, expected %u)\n", millis(), version, CSS_CACHE_VERSION); + return false; + } + + // Read rule count + uint16_t ruleCount = 0; + if (file.read(&ruleCount, sizeof(ruleCount)) != sizeof(ruleCount)) { + return false; + } + + // Read each rule + for (uint16_t i = 0; i < ruleCount; ++i) { + // Read selector string + uint16_t selectorLen = 0; + if (file.read(&selectorLen, sizeof(selectorLen)) != sizeof(selectorLen)) { + rulesBySelector_.clear(); + return false; + } + + std::string selector; + selector.resize(selectorLen); + if (file.read(&selector[0], selectorLen) != selectorLen) { + rulesBySelector_.clear(); + return false; + } + + // Read CssStyle fields + CssStyle style; + uint8_t enumVal; + + if (file.read(&enumVal, 1) != 1) { + rulesBySelector_.clear(); + return false; + } + style.textAlign = static_cast(enumVal); + + if (file.read(&enumVal, 1) != 1) { + rulesBySelector_.clear(); + return false; + } + style.fontStyle = static_cast(enumVal); + + if (file.read(&enumVal, 1) != 1) { + rulesBySelector_.clear(); + return false; + } + style.fontWeight = static_cast(enumVal); + + if (file.read(&enumVal, 1) != 1) { + rulesBySelector_.clear(); + return false; + } + style.textDecoration = static_cast(enumVal); + + // Read CssLength fields + auto readLength = [&file](CssLength& len) -> bool { + if (file.read(&len.value, sizeof(len.value)) != sizeof(len.value)) { + return false; + } + uint8_t unitVal; + if (file.read(&unitVal, 1) != 1) { + return false; + } + len.unit = static_cast(unitVal); + return true; + }; + + if (!readLength(style.textIndent) || !readLength(style.marginTop) || !readLength(style.marginBottom) || + !readLength(style.marginLeft) || !readLength(style.marginRight) || !readLength(style.paddingTop) || + !readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight)) { + rulesBySelector_.clear(); + return false; + } + + // Read defined flags + uint16_t definedBits = 0; + if (file.read(&definedBits, sizeof(definedBits)) != sizeof(definedBits)) { + rulesBySelector_.clear(); + return false; + } + style.defined.textAlign = (definedBits & 1 << 0) != 0; + style.defined.fontStyle = (definedBits & 1 << 1) != 0; + style.defined.fontWeight = (definedBits & 1 << 2) != 0; + style.defined.textDecoration = (definedBits & 1 << 3) != 0; + style.defined.textIndent = (definedBits & 1 << 4) != 0; + style.defined.marginTop = (definedBits & 1 << 5) != 0; + style.defined.marginBottom = (definedBits & 1 << 6) != 0; + style.defined.marginLeft = (definedBits & 1 << 7) != 0; + style.defined.marginRight = (definedBits & 1 << 8) != 0; + style.defined.paddingTop = (definedBits & 1 << 9) != 0; + style.defined.paddingBottom = (definedBits & 1 << 10) != 0; + style.defined.paddingLeft = (definedBits & 1 << 11) != 0; + style.defined.paddingRight = (definedBits & 1 << 12) != 0; + + rulesBySelector_[selector] = style; + } + + Serial.printf("[%lu] [CSS] Loaded %u rules from cache\n", millis(), ruleCount); + return true; +} diff --git a/lib/Epub/Epub/css/CssParser.h b/lib/Epub/Epub/css/CssParser.h index 9915485d..0e5a1b34 100644 --- a/lib/Epub/Epub/css/CssParser.h +++ b/lib/Epub/Epub/css/CssParser.h @@ -76,6 +76,21 @@ class CssParser { */ void clear() { rulesBySelector_.clear(); } + /** + * 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; + + /** + * 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); + private: // Storage: maps normalized selector -> style properties std::unordered_map rulesBySelector_;