feat: implement CSS enumeration through OPF directory when missing from manifest (#2148)
This commit is contained in:
+40
-1
@@ -255,6 +255,38 @@ bool Epub::parseTocNavFile() const {
|
|||||||
return true;
|
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 {
|
void Epub::parseCssFiles() const {
|
||||||
// Maximum CSS file size we'll attempt to parse (uncompressed)
|
// Maximum CSS file size we'll attempt to parse (uncompressed)
|
||||||
// Larger files risk memory exhaustion on ESP32
|
// Larger files risk memory exhaustion on ESP32
|
||||||
@@ -329,9 +361,9 @@ void Epub::parseCssFiles() const {
|
|||||||
if (!cssParser->saveToCache()) {
|
if (!cssParser->saveToCache()) {
|
||||||
LOG_ERR("EBP", "Failed to save CSS rules to cache");
|
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());
|
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
|
// 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)) {
|
if (!parseContentOpf(bookMetadataCache->coreMetadata)) {
|
||||||
LOG_ERR("EBP", "Could not parse content.opf from cached bookMetadata for CSS files");
|
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
|
// 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();
|
parseCssFiles();
|
||||||
// Invalidate section caches so they are rebuilt with the new CSS
|
// 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) {
|
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
|
// Parse CSS files after cache reload
|
||||||
parseCssFiles();
|
parseCssFiles();
|
||||||
Storage.removeDir((cachePath + "/sections").c_str());
|
Storage.removeDir((cachePath + "/sections").c_str());
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class Epub {
|
|||||||
bool parseTocNcxFile() const;
|
bool parseTocNcxFile() const;
|
||||||
bool parseTocNavFile() const;
|
bool parseTocNavFile() const;
|
||||||
void parseCssFiles() const;
|
void parseCssFiles() const;
|
||||||
|
void discoverCssFilesFromZip();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ bool hasTxtExtension(std::string_view fileName) { return checkFileExtension(file
|
|||||||
|
|
||||||
bool hasMarkdownExtension(std::string_view fileName) { return checkFileExtension(fileName, ".md"); }
|
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) {
|
std::string extractFolderPath(const std::string& filePath) {
|
||||||
const auto lastSlash = filePath.find_last_of('/');
|
const auto lastSlash = filePath.find_last_of('/');
|
||||||
if (lastSlash == std::string::npos || lastSlash == 0) {
|
if (lastSlash == std::string::npos || lastSlash == 0) {
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ inline bool hasTxtExtension(const String& fileName) {
|
|||||||
// Check for .md extension (case-insensitive)
|
// Check for .md extension (case-insensitive)
|
||||||
bool hasMarkdownExtension(std::string_view fileName);
|
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);
|
std::string extractFolderPath(const std::string& filePath);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
class ZipFile {
|
class ZipFile {
|
||||||
@@ -69,4 +70,11 @@ class ZipFile {
|
|||||||
// These functions will open and close the zip as needed
|
// These functions will open and close the zip as needed
|
||||||
uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false);
|
uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false);
|
||||||
bool readFileToStream(const char* filename, Print& out, size_t chunkSize);
|
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});
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user