feat: implement CSS enumeration through OPF directory when missing from manifest (#2148)

This commit is contained in:
Jacob Latonis
2026-05-25 21:23:19 -04:00
committed by GitHub
parent 94d4b0c7bb
commit f4e7eaa198
5 changed files with 56 additions and 1 deletions
+40 -1
View File
@@ -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());
+1
View File
@@ -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)) {
+2
View File
@@ -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) {
+5
View File
@@ -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);
/**
+8
View File
@@ -3,6 +3,7 @@
#include <deque>
#include <string>
#include <string_view>
#include <unordered_map>
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 <typename F>
void enumerateFilePaths(F&& callback) const {
for (const auto& entry : fileStatSlimCache) {
callback(std::string_view{entry.first});
}
}
};