From f4e7eaa19852a05af7779a276e159de5bbb97051 Mon Sep 17 00:00:00 2001 From: Jacob Latonis Date: Mon, 25 May 2026 21:23:19 -0400 Subject: [PATCH] feat: implement CSS enumeration through OPF directory when missing from manifest (#2148) --- lib/Epub/Epub.cpp | 41 ++++++++++++++++++++++++++++++++++++- lib/Epub/Epub.h | 1 + lib/FsHelpers/FsHelpers.cpp | 2 ++ lib/FsHelpers/FsHelpers.h | 5 +++++ lib/ZipFile/ZipFile.h | 8 ++++++++ 5 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 7ca320d5..7cf713f6 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -255,6 +255,38 @@ bool Epub::parseTocNavFile() const { return true; } +void Epub::discoverCssFilesFromZip() { + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { + LOG_ERR("EBP", "Cannot discover CSS from ZIP because book metadata cache is not loaded"); + return; + } + + ZipFile zf(filepath); + + if (!zf.loadAllFileStatSlims()) { + LOG_ERR("EBP", "Failed to load ZIP file stat slims for CSS discovery"); + return; + } + + size_t lastSlash = contentBasePath.find_last_of('/'); + + std::string opfDir = (lastSlash != std::string::npos) ? contentBasePath.substr(0, lastSlash + 1) : ""; + + zf.enumerateFilePaths([&](std::string_view filePath) { + if (!opfDir.empty() && filePath.find(opfDir) != 0) { + return; // Skip files that are not in the same directory as OPF manifest, as CSS files are typically located + // there or in subfolders + } + + if (FsHelpers::hasCssExtension(filePath)) { + if (std::find(cssFiles.begin(), cssFiles.end(), filePath) == cssFiles.end()) { + LOG_DBG("EBP", "Discovered CSS file via ZIP enumeration: %.*s", (int)filePath.size(), filePath.data()); + cssFiles.push_back(std::string{filePath}); + } + } + }); +} + void Epub::parseCssFiles() const { // Maximum CSS file size we'll attempt to parse (uncompressed) // Larger files risk memory exhaustion on ESP32 @@ -329,9 +361,9 @@ void Epub::parseCssFiles() const { 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()); + cssParser->clear(); } // load in the meta data for the epub file @@ -354,6 +386,10 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) { if (!parseContentOpf(bookMetadataCache->coreMetadata)) { LOG_ERR("EBP", "Could not parse content.opf from cached bookMetadata for CSS files"); // continue anyway - book will work without CSS and we'll still load any inline style CSS + } else { + // Handle case where CSS files are not listed in OPF manifest + // but are still referenced by HTML files - discover and parse them too + discoverCssFilesFromZip(); } parseCssFiles(); // Invalidate section caches so they are rebuilt with the new CSS @@ -457,6 +493,9 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) { } if (!skipLoadingCss) { + // Handle case where CSS files are not listed in OPF manifest + // but are still referenced by HTML files - discover and parse them too + discoverCssFilesFromZip(); // Parse CSS files after cache reload parseCssFiles(); Storage.removeDir((cachePath + "/sections").c_str()); diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index 9ffa8d37..2ae95c35 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -35,6 +35,7 @@ class Epub { bool parseTocNcxFile() const; bool parseTocNavFile() const; void parseCssFiles() const; + void discoverCssFilesFromZip(); public: explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) { diff --git a/lib/FsHelpers/FsHelpers.cpp b/lib/FsHelpers/FsHelpers.cpp index a73f8901..fe48587f 100644 --- a/lib/FsHelpers/FsHelpers.cpp +++ b/lib/FsHelpers/FsHelpers.cpp @@ -129,6 +129,8 @@ bool hasTxtExtension(std::string_view fileName) { return checkFileExtension(file bool hasMarkdownExtension(std::string_view fileName) { return checkFileExtension(fileName, ".md"); } +bool hasCssExtension(std::string_view fileName) { return checkFileExtension(fileName, ".css"); } + std::string extractFolderPath(const std::string& filePath) { const auto lastSlash = filePath.find_last_of('/'); if (lastSlash == std::string::npos || lastSlash == 0) { diff --git a/lib/FsHelpers/FsHelpers.h b/lib/FsHelpers/FsHelpers.h index 56c2c987..aa9c44f4 100644 --- a/lib/FsHelpers/FsHelpers.h +++ b/lib/FsHelpers/FsHelpers.h @@ -58,6 +58,11 @@ inline bool hasTxtExtension(const String& fileName) { // Check for .md extension (case-insensitive) bool hasMarkdownExtension(std::string_view fileName); +// Check for .css extension (case-insensitive) +bool hasCssExtension(std::string_view fileName); +inline bool hasCssExtension(const String& fileName) { + return hasCssExtension(std::string_view{fileName.c_str(), fileName.length()}); +} std::string extractFolderPath(const std::string& filePath); /** diff --git a/lib/ZipFile/ZipFile.h b/lib/ZipFile/ZipFile.h index 2a32e974..6c4f9396 100644 --- a/lib/ZipFile/ZipFile.h +++ b/lib/ZipFile/ZipFile.h @@ -3,6 +3,7 @@ #include #include +#include #include class ZipFile { @@ -69,4 +70,11 @@ class ZipFile { // These functions will open and close the zip as needed uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false); bool readFileToStream(const char* filename, Print& out, size_t chunkSize); + + template + void enumerateFilePaths(F&& callback) const { + for (const auto& entry : fileStatSlimCache) { + callback(std::string_view{entry.first}); + } + } };