Remove tinyxml2 dependency replace with expat parsers (#9)
This commit is contained in:
13
lib/Epub/Epub/EpubTocEntry.h
Normal file
13
lib/Epub/Epub/EpubTocEntry.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
class EpubTocEntry {
|
||||
public:
|
||||
std::string title;
|
||||
std::string href;
|
||||
std::string anchor;
|
||||
int level;
|
||||
EpubTocEntry(std::string title, std::string href, std::string anchor, const int level)
|
||||
: title(std::move(title)), href(std::move(href)), anchor(std::move(anchor)), level(level) {}
|
||||
};
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include "EpubHtmlParserSlim.h"
|
||||
#include "FsHelpers.h"
|
||||
#include "Page.h"
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 4;
|
||||
|
||||
@@ -127,9 +127,9 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression,
|
||||
|
||||
const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath;
|
||||
|
||||
EpubHtmlParserSlim visitor(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight,
|
||||
marginBottom, marginLeft,
|
||||
[this](std::unique_ptr<Page> page) { this->onPageComplete(std::move(page)); });
|
||||
ChapterHtmlSlimParser visitor(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight,
|
||||
marginBottom, marginLeft,
|
||||
[this](std::unique_ptr<Page> page) { this->onPageComplete(std::move(page)); });
|
||||
success = visitor.parseAndBuildPages();
|
||||
|
||||
SD.remove(tmpHtmlPath.c_str());
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#include "EpubHtmlParserSlim.h"
|
||||
#include "ChapterHtmlSlimParser.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <expat.h>
|
||||
|
||||
#include "Page.h"
|
||||
#include "htmlEntities.h"
|
||||
#include "../Page.h"
|
||||
#include "../htmlEntities.h"
|
||||
|
||||
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
||||
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
||||
@@ -38,7 +38,7 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib
|
||||
}
|
||||
|
||||
// start a new text block if needed
|
||||
void EpubHtmlParserSlim::startNewTextBlock(const TextBlock::BLOCK_STYLE style) {
|
||||
void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::BLOCK_STYLE style) {
|
||||
if (currentTextBlock) {
|
||||
// already have a text block running and it is empty - just reuse it
|
||||
if (currentTextBlock->isEmpty()) {
|
||||
@@ -51,8 +51,8 @@ void EpubHtmlParserSlim::startNewTextBlock(const TextBlock::BLOCK_STYLE style) {
|
||||
currentTextBlock.reset(new ParsedText(style));
|
||||
}
|
||||
|
||||
void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||
auto* self = static_cast<EpubHtmlParserSlim*>(userData);
|
||||
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||
(void)atts;
|
||||
|
||||
// Middle of skip
|
||||
@@ -62,23 +62,7 @@ void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* na
|
||||
}
|
||||
|
||||
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
||||
// const char* src = element.Attribute("src");
|
||||
// if (src) {
|
||||
// // don't leave an empty text block in the list
|
||||
// // const BLOCK_STYLE style = currentTextBlock->get_style();
|
||||
// if (currentTextBlock->isEmpty()) {
|
||||
// delete currentTextBlock;
|
||||
// currentTextBlock = nullptr;
|
||||
// }
|
||||
// // TODO: Fix this
|
||||
// // blocks.push_back(new ImageBlock(m_base_path + src));
|
||||
// // start a new text block - with the same style as before
|
||||
// // startNewTextBlock(style);
|
||||
// } else {
|
||||
// // ESP_LOGE(TAG, "Could not find src attribute");
|
||||
// }
|
||||
|
||||
// start skip
|
||||
// TODO: Start processing image tags
|
||||
self->skipUntilDepth = self->depth;
|
||||
self->depth += 1;
|
||||
return;
|
||||
@@ -109,8 +93,8 @@ void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* na
|
||||
self->depth += 1;
|
||||
}
|
||||
|
||||
void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s, const int len) {
|
||||
auto* self = static_cast<EpubHtmlParserSlim*>(userData);
|
||||
void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char* s, const int len) {
|
||||
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||
|
||||
// Middle of skip
|
||||
if (self->skipUntilDepth < self->depth) {
|
||||
@@ -149,8 +133,8 @@ void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name) {
|
||||
auto* self = static_cast<EpubHtmlParserSlim*>(userData);
|
||||
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
|
||||
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||
(void)name;
|
||||
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
@@ -196,7 +180,7 @@ void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name
|
||||
}
|
||||
}
|
||||
|
||||
bool EpubHtmlParserSlim::parseAndBuildPages() {
|
||||
bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
startNewTextBlock(TextBlock::JUSTIFIED);
|
||||
|
||||
const XML_Parser parser = XML_ParserCreate(nullptr);
|
||||
@@ -261,7 +245,7 @@ bool EpubHtmlParserSlim::parseAndBuildPages() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void EpubHtmlParserSlim::makePages() {
|
||||
void ChapterHtmlSlimParser::makePages() {
|
||||
if (!currentTextBlock) {
|
||||
Serial.printf("[%lu] [EHP] !! No text block to make pages for !!\n", millis());
|
||||
return;
|
||||
@@ -6,15 +6,15 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include "ParsedText.h"
|
||||
#include "blocks/TextBlock.h"
|
||||
#include "../ParsedText.h"
|
||||
#include "../blocks/TextBlock.h"
|
||||
|
||||
class Page;
|
||||
class GfxRenderer;
|
||||
|
||||
#define MAX_WORD_SIZE 200
|
||||
|
||||
class EpubHtmlParserSlim {
|
||||
class ChapterHtmlSlimParser {
|
||||
const char* filepath;
|
||||
GfxRenderer& renderer;
|
||||
std::function<void(std::unique_ptr<Page>)> completePageFn;
|
||||
@@ -44,10 +44,10 @@ class EpubHtmlParserSlim {
|
||||
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
||||
|
||||
public:
|
||||
explicit EpubHtmlParserSlim(const char* filepath, GfxRenderer& renderer, const int fontId,
|
||||
const float lineCompression, const int marginTop, const int marginRight,
|
||||
const int marginBottom, const int marginLeft,
|
||||
const std::function<void(std::unique_ptr<Page>)>& completePageFn)
|
||||
explicit ChapterHtmlSlimParser(const char* filepath, GfxRenderer& renderer, const int fontId,
|
||||
const float lineCompression, const int marginTop, const int marginRight,
|
||||
const int marginBottom, const int marginLeft,
|
||||
const std::function<void(std::unique_ptr<Page>)>& completePageFn)
|
||||
: filepath(filepath),
|
||||
renderer(renderer),
|
||||
fontId(fontId),
|
||||
@@ -57,6 +57,6 @@ class EpubHtmlParserSlim {
|
||||
marginBottom(marginBottom),
|
||||
marginLeft(marginLeft),
|
||||
completePageFn(completePageFn) {}
|
||||
~EpubHtmlParserSlim() = default;
|
||||
~ChapterHtmlSlimParser() = default;
|
||||
bool parseAndBuildPages();
|
||||
};
|
||||
96
lib/Epub/Epub/parsers/ContainerParser.cpp
Normal file
96
lib/Epub/Epub/parsers/ContainerParser.cpp
Normal file
@@ -0,0 +1,96 @@
|
||||
#include "ContainerParser.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
bool ContainerParser::setup() {
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
Serial.printf("[%lu] [CTR] Couldn't allocate memory for parser\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
XML_SetUserData(parser, this);
|
||||
XML_SetElementHandler(parser, startElement, endElement);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ContainerParser::teardown() {
|
||||
if (parser) {
|
||||
XML_ParserFree(parser);
|
||||
parser = nullptr;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t ContainerParser::write(const uint8_t data) { return write(&data, 1); }
|
||||
|
||||
size_t ContainerParser::write(const uint8_t* buffer, const size_t size) {
|
||||
if (!parser) return 0;
|
||||
|
||||
const uint8_t* currentBufferPos = buffer;
|
||||
auto remainingInBuffer = size;
|
||||
|
||||
while (remainingInBuffer > 0) {
|
||||
void* const buf = XML_GetBuffer(parser, 1024);
|
||||
if (!buf) {
|
||||
Serial.printf("[%lu] [CTR] Couldn't allocate buffer\n", millis());
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
|
||||
memcpy(buf, currentBufferPos, toRead);
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||
Serial.printf("[%lu] [CTR] Parse error: %s\n", millis(), XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
currentBufferPos += toRead;
|
||||
remainingInBuffer -= toRead;
|
||||
remainingSize -= toRead;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
void XMLCALL ContainerParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||
auto* self = static_cast<ContainerParser*>(userData);
|
||||
|
||||
// Simple state tracking to ensure we are looking at the valid schema structure
|
||||
if (self->state == START && strcmp(name, "container") == 0) {
|
||||
self->state = IN_CONTAINER;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_CONTAINER && strcmp(name, "rootfiles") == 0) {
|
||||
self->state = IN_ROOTFILES;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_ROOTFILES && strcmp(name, "rootfile") == 0) {
|
||||
const char* mediaType = nullptr;
|
||||
const char* path = nullptr;
|
||||
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "media-type") == 0) {
|
||||
mediaType = atts[i + 1];
|
||||
} else if (strcmp(atts[i], "full-path") == 0) {
|
||||
path = atts[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is the standard OEBPS package
|
||||
if (mediaType && path && strcmp(mediaType, "application/oebps-package+xml") == 0) {
|
||||
self->fullPath = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL ContainerParser::endElement(void* userData, const XML_Char* name) {
|
||||
auto* self = static_cast<ContainerParser*>(userData);
|
||||
|
||||
if (self->state == IN_ROOTFILES && strcmp(name, "rootfiles") == 0) {
|
||||
self->state = IN_CONTAINER;
|
||||
} else if (self->state == IN_CONTAINER && strcmp(name, "container") == 0) {
|
||||
self->state = START;
|
||||
}
|
||||
}
|
||||
32
lib/Epub/Epub/parsers/ContainerParser.h
Normal file
32
lib/Epub/Epub/parsers/ContainerParser.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include <Print.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "expat.h"
|
||||
|
||||
class ContainerParser final : public Print {
|
||||
enum ParserState {
|
||||
START,
|
||||
IN_CONTAINER,
|
||||
IN_ROOTFILES,
|
||||
};
|
||||
|
||||
size_t remainingSize;
|
||||
XML_Parser parser = nullptr;
|
||||
ParserState state = START;
|
||||
|
||||
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||
static void endElement(void* userData, const XML_Char* name);
|
||||
|
||||
public:
|
||||
std::string fullPath;
|
||||
|
||||
explicit ContainerParser(const size_t xmlSize) : remainingSize(xmlSize) {}
|
||||
|
||||
bool setup();
|
||||
bool teardown();
|
||||
|
||||
size_t write(uint8_t) override;
|
||||
size_t write(const uint8_t* buffer, size_t size) override;
|
||||
};
|
||||
161
lib/Epub/Epub/parsers/ContentOpfParser.cpp
Normal file
161
lib/Epub/Epub/parsers/ContentOpfParser.cpp
Normal file
@@ -0,0 +1,161 @@
|
||||
#include "ContentOpfParser.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <ZipFile.h>
|
||||
|
||||
bool ContentOpfParser::setup() {
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
Serial.printf("[%lu] [COF] Couldn't allocate memory for parser\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
XML_SetUserData(parser, this);
|
||||
XML_SetElementHandler(parser, startElement, endElement);
|
||||
XML_SetCharacterDataHandler(parser, characterData);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ContentOpfParser::teardown() {
|
||||
if (parser) {
|
||||
XML_ParserFree(parser);
|
||||
parser = nullptr;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); }
|
||||
|
||||
size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) {
|
||||
if (!parser) return 0;
|
||||
|
||||
const uint8_t* currentBufferPos = buffer;
|
||||
auto remainingInBuffer = size;
|
||||
|
||||
while (remainingInBuffer > 0) {
|
||||
void* const buf = XML_GetBuffer(parser, 1024);
|
||||
|
||||
if (!buf) {
|
||||
Serial.printf("[%lu] [COF] Couldn't allocate memory for buffer\n", millis());
|
||||
XML_ParserFree(parser);
|
||||
parser = nullptr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
|
||||
memcpy(buf, currentBufferPos, toRead);
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||
Serial.printf("[%lu] [COF] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
XML_ParserFree(parser);
|
||||
parser = nullptr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
currentBufferPos += toRead;
|
||||
remainingInBuffer -= toRead;
|
||||
remainingSize -= toRead;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||
auto* self = static_cast<ContentOpfParser*>(userData);
|
||||
(void)atts;
|
||||
|
||||
if (self->state == START && (strcmp(name, "package") == 0 || strcmp(name, "opf:package") == 0)) {
|
||||
self->state = IN_PACKAGE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_PACKAGE && (strcmp(name, "metadata") == 0 || strcmp(name, "opf:metadata") == 0)) {
|
||||
self->state = IN_METADATA;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_METADATA && strcmp(name, "dc:title") == 0) {
|
||||
self->state = IN_BOOK_TITLE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
||||
self->state = IN_MANIFEST;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
|
||||
self->state = IN_SPINE;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Support book cover
|
||||
// if (self->state == IN_METADATA && (strcmp(name, "meta") == 0 || strcmp(name, "opf:meta") == 0)) {
|
||||
// }
|
||||
|
||||
if (self->state == IN_MANIFEST && (strcmp(name, "item") == 0 || strcmp(name, "opf:item") == 0)) {
|
||||
std::string itemId;
|
||||
std::string href;
|
||||
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "id") == 0) {
|
||||
itemId = atts[i + 1];
|
||||
} else if (strcmp(atts[i], "href") == 0) {
|
||||
href = self->baseContentPath + atts[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
self->items[itemId] = href;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_SPINE && (strcmp(name, "itemref") == 0 || strcmp(name, "opf:itemref") == 0)) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "idref") == 0) {
|
||||
self->spineRefs.emplace_back(atts[i + 1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s, const int len) {
|
||||
auto* self = static_cast<ContentOpfParser*>(userData);
|
||||
|
||||
if (self->state == IN_BOOK_TITLE) {
|
||||
self->title.append(s, len);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name) {
|
||||
auto* self = static_cast<ContentOpfParser*>(userData);
|
||||
(void)name;
|
||||
|
||||
if (self->state == IN_SPINE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
|
||||
self->state = IN_PACKAGE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_MANIFEST && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
||||
self->state = IN_PACKAGE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_BOOK_TITLE && strcmp(name, "dc:title") == 0) {
|
||||
self->state = IN_METADATA;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_METADATA && (strcmp(name, "metadata") == 0 || strcmp(name, "opf:metadata") == 0)) {
|
||||
self->state = IN_PACKAGE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_PACKAGE && (strcmp(name, "package") == 0 || strcmp(name, "opf:package") == 0)) {
|
||||
self->state = START;
|
||||
return;
|
||||
}
|
||||
}
|
||||
42
lib/Epub/Epub/parsers/ContentOpfParser.h
Normal file
42
lib/Epub/Epub/parsers/ContentOpfParser.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
#include <Print.h>
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "Epub.h"
|
||||
#include "expat.h"
|
||||
|
||||
class ContentOpfParser final : public Print {
|
||||
enum ParserState {
|
||||
START,
|
||||
IN_PACKAGE,
|
||||
IN_METADATA,
|
||||
IN_BOOK_TITLE,
|
||||
IN_MANIFEST,
|
||||
IN_SPINE,
|
||||
};
|
||||
|
||||
const std::string& baseContentPath;
|
||||
size_t remainingSize;
|
||||
XML_Parser parser = nullptr;
|
||||
ParserState state = START;
|
||||
|
||||
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||
static void characterData(void* userData, const XML_Char* s, int len);
|
||||
static void endElement(void* userData, const XML_Char* name);
|
||||
|
||||
public:
|
||||
std::string title;
|
||||
std::string tocNcxPath;
|
||||
std::map<std::string, std::string> items;
|
||||
std::vector<std::string> spineRefs;
|
||||
|
||||
explicit ContentOpfParser(const std::string& baseContentPath, const size_t xmlSize)
|
||||
: baseContentPath(baseContentPath), remainingSize(xmlSize) {}
|
||||
|
||||
bool setup();
|
||||
bool teardown();
|
||||
|
||||
size_t write(uint8_t) override;
|
||||
size_t write(const uint8_t* buffer, size_t size) override;
|
||||
};
|
||||
165
lib/Epub/Epub/parsers/TocNcxParser.cpp
Normal file
165
lib/Epub/Epub/parsers/TocNcxParser.cpp
Normal file
@@ -0,0 +1,165 @@
|
||||
#include "TocNcxParser.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
bool TocNcxParser::setup() {
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
Serial.printf("[%lu] [TOC] Couldn't allocate memory for parser\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
XML_SetUserData(parser, this);
|
||||
XML_SetElementHandler(parser, startElement, endElement);
|
||||
XML_SetCharacterDataHandler(parser, characterData);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TocNcxParser::teardown() {
|
||||
if (parser) {
|
||||
XML_ParserFree(parser);
|
||||
parser = nullptr;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t TocNcxParser::write(const uint8_t data) { return write(&data, 1); }
|
||||
|
||||
size_t TocNcxParser::write(const uint8_t* buffer, const size_t size) {
|
||||
if (!parser) return 0;
|
||||
|
||||
const uint8_t* currentBufferPos = buffer;
|
||||
auto remainingInBuffer = size;
|
||||
|
||||
while (remainingInBuffer > 0) {
|
||||
void* const buf = XML_GetBuffer(parser, 1024);
|
||||
if (!buf) {
|
||||
Serial.printf("[%lu] [TOC] Couldn't allocate memory for buffer\n", millis());
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
|
||||
memcpy(buf, currentBufferPos, toRead);
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||
Serial.printf("[%lu] [TOC] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
currentBufferPos += toRead;
|
||||
remainingInBuffer -= toRead;
|
||||
remainingSize -= toRead;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
void XMLCALL TocNcxParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||
// NOTE: We rely on navPoint label and content coming before any nested navPoints, this will be fine:
|
||||
// <navPoint>
|
||||
// <navLabel><text>Chapter 1</text></navLabel>
|
||||
// <content src="ch1.html"/>
|
||||
// <navPoint> ...nested... </navPoint>
|
||||
// </navPoint>
|
||||
//
|
||||
// This will NOT:
|
||||
// <navPoint>
|
||||
// <navPoint> ...nested... </navPoint>
|
||||
// <navLabel><text>Chapter 1</text></navLabel>
|
||||
// <content src="ch1.html"/>
|
||||
// </navPoint>
|
||||
|
||||
auto* self = static_cast<TocNcxParser*>(userData);
|
||||
|
||||
if (self->state == START && strcmp(name, "ncx") == 0) {
|
||||
self->state = IN_NCX;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_NCX && strcmp(name, "navMap") == 0) {
|
||||
self->state = IN_NAV_MAP;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handles both top-level and nested navPoints
|
||||
if ((self->state == IN_NAV_MAP || self->state == IN_NAV_POINT) && strcmp(name, "navPoint") == 0) {
|
||||
self->state = IN_NAV_POINT;
|
||||
self->currentDepth++;
|
||||
|
||||
self->currentLabel.clear();
|
||||
self->currentSrc.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_NAV_POINT && strcmp(name, "navLabel") == 0) {
|
||||
self->state = IN_NAV_LABEL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_NAV_LABEL && strcmp(name, "text") == 0) {
|
||||
self->state = IN_NAV_LABEL_TEXT;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_NAV_POINT && strcmp(name, "content") == 0) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "src") == 0) {
|
||||
self->currentSrc = atts[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL TocNcxParser::characterData(void* userData, const XML_Char* s, const int len) {
|
||||
auto* self = static_cast<TocNcxParser*>(userData);
|
||||
if (self->state == IN_NAV_LABEL_TEXT) {
|
||||
self->currentLabel.append(s, len);
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL TocNcxParser::endElement(void* userData, const XML_Char* name) {
|
||||
auto* self = static_cast<TocNcxParser*>(userData);
|
||||
|
||||
if (self->state == IN_NAV_LABEL_TEXT && strcmp(name, "text") == 0) {
|
||||
self->state = IN_NAV_LABEL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_NAV_LABEL && strcmp(name, "navLabel") == 0) {
|
||||
self->state = IN_NAV_POINT;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_NAV_POINT && strcmp(name, "navPoint") == 0) {
|
||||
self->currentDepth--;
|
||||
if (self->currentDepth == 0) {
|
||||
self->state = IN_NAV_MAP;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_NAV_POINT && strcmp(name, "content") == 0) {
|
||||
// At this point (end of content tag), we likely have both Label (from previous tags) and Src.
|
||||
// This is the safest place to push the data, assuming <navLabel> always comes before <content>.
|
||||
// NCX spec says navLabel comes before content.
|
||||
if (!self->currentLabel.empty() && !self->currentSrc.empty()) {
|
||||
std::string href = self->baseContentPath + self->currentSrc;
|
||||
std::string anchor;
|
||||
|
||||
const size_t pos = href.find('#');
|
||||
if (pos != std::string::npos) {
|
||||
anchor = href.substr(pos + 1);
|
||||
href = href.substr(0, pos);
|
||||
}
|
||||
|
||||
// Push to vector
|
||||
self->toc.emplace_back(self->currentLabel, href, anchor, self->currentDepth);
|
||||
|
||||
// Clear them so we don't re-add them if there are weird XML structures
|
||||
self->currentLabel.clear();
|
||||
self->currentSrc.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
37
lib/Epub/Epub/parsers/TocNcxParser.h
Normal file
37
lib/Epub/Epub/parsers/TocNcxParser.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#include <Print.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Epub/EpubTocEntry.h"
|
||||
#include "expat.h"
|
||||
|
||||
class TocNcxParser final : public Print {
|
||||
enum ParserState { START, IN_NCX, IN_NAV_MAP, IN_NAV_POINT, IN_NAV_LABEL, IN_NAV_LABEL_TEXT, IN_CONTENT };
|
||||
|
||||
const std::string& baseContentPath;
|
||||
size_t remainingSize;
|
||||
XML_Parser parser = nullptr;
|
||||
ParserState state = START;
|
||||
|
||||
std::string currentLabel;
|
||||
std::string currentSrc;
|
||||
size_t currentDepth = 0;
|
||||
|
||||
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||
static void characterData(void* userData, const XML_Char* s, int len);
|
||||
static void endElement(void* userData, const XML_Char* name);
|
||||
|
||||
public:
|
||||
std::vector<EpubTocEntry> toc;
|
||||
|
||||
explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize)
|
||||
: baseContentPath(baseContentPath), remainingSize(xmlSize) {}
|
||||
|
||||
bool setup();
|
||||
bool teardown();
|
||||
|
||||
size_t write(uint8_t) override;
|
||||
size_t write(const uint8_t* buffer, size_t size) override;
|
||||
};
|
||||
Reference in New Issue
Block a user