Lettore 0.3.0: bookmark per posizione (carattere) e paginazione corretta
- Bookmark cw:v2:<offset assoluto caratteri> invece di capitolo+frazione; migrazione automatica delle chiavi v1 (parseBookmarkKey) - Lunghezze testo per capitolo in C++ (text node del DOM, entità decodificate, body-only) per mappare offset assoluto -> capitolo - Paginazione: stile iniettato height:auto/overflow:visible + unlock JS dei contenitori hidden — il CSS di molti EPUB bloccava lo scroll e ogni tap saltava al capitolo successivo - prevPage dall'inizio capitolo -> ultima pagina del capitolo precedente - Cambio font mantiene la posizione (restore per carattere) - Label menu: pagina corrente/totale del capitolo; max su documentElement+body - Test: harness C++ (Qt) e test JS con DOM mock
This commit is contained in:
+153
-10
@@ -117,6 +117,82 @@ QByteArray extractEntryData(const QByteArray &data, const ZipEntry &e)
|
||||
return QByteArray(); // metodo non supportato
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lunghezza del testo VISIBILE di un capitolo (criterio DOM/JS):
|
||||
// tag rimossi, entità decodificate (1 carattere), head/script/style/noscript/
|
||||
// title esclusi, spazi e newline contati come nel text node del browser.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int plainTextLength(const QString &html)
|
||||
{
|
||||
// conta solo il testo dentro <body> (come il walker JS che parte da
|
||||
// document.body): l'header XML/DOCTYPE e il <head> non contano
|
||||
const int bodyStart = html.indexOf(QStringLiteral("<body"), 0, Qt::CaseInsensitive);
|
||||
const int bodyEnd = html.indexOf(QStringLiteral("</body"), 0, Qt::CaseInsensitive);
|
||||
const QString scope = (bodyStart >= 0)
|
||||
? html.mid(bodyStart, (bodyEnd >= bodyStart ? bodyEnd : html.size()) - bodyStart)
|
||||
: html;
|
||||
|
||||
int count = 0;
|
||||
int i = 0;
|
||||
const int n = scope.size();
|
||||
while (i < n) {
|
||||
const QChar c = scope.at(i);
|
||||
if (c == QLatin1Char('<')) {
|
||||
const int close = scope.indexOf(QLatin1Char('>'), i + 1);
|
||||
if (close < 0) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
QString tag = scope.mid(i + 1, close - i - 1).trimmed().toLower();
|
||||
if (tag.startsWith(QLatin1String("!--"))) {
|
||||
const int end = scope.indexOf(QLatin1String("-->"), close);
|
||||
i = (end < 0) ? n : end + 3;
|
||||
continue;
|
||||
}
|
||||
if (tag.startsWith(QLatin1Char('/')))
|
||||
tag = tag.mid(1);
|
||||
int sp = 0;
|
||||
while (sp < tag.size() && tag.at(sp).isLetterOrNumber())
|
||||
++sp;
|
||||
const QString name = tag.left(sp);
|
||||
if (name == QLatin1String("script") || name == QLatin1String("style")
|
||||
|| name == QLatin1String("noscript") || name == QLatin1String("head")
|
||||
|| name == QLatin1String("title")) {
|
||||
// salta tutto il contenuto (e la chiusura) del blocco raw
|
||||
const QString closeTag = QStringLiteral("</") + name;
|
||||
const int end = scope.indexOf(closeTag, close, Qt::CaseInsensitive);
|
||||
if (end < 0) {
|
||||
i = n;
|
||||
continue;
|
||||
}
|
||||
const int gt = scope.indexOf(QLatin1Char('>'), end);
|
||||
i = (gt < 0) ? n : gt + 1;
|
||||
continue;
|
||||
}
|
||||
i = close + 1;
|
||||
continue;
|
||||
}
|
||||
if (c == QLatin1Char('&')) {
|
||||
const int semi = scope.indexOf(QLatin1Char(';'), i + 1);
|
||||
if (semi > i && semi - i <= 10) {
|
||||
const QString ent = scope.mid(i + 1, semi - i - 1);
|
||||
if (ent == QLatin1String("amp") || ent == QLatin1String("lt")
|
||||
|| ent == QLatin1String("gt") || ent == QLatin1String("quot")
|
||||
|| ent == QLatin1String("apos") || ent == QLatin1String("nbsp")
|
||||
|| ent.startsWith(QLatin1String("#"))) {
|
||||
++count;
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
++count;
|
||||
++i;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -140,6 +216,7 @@ void EpubBook::close()
|
||||
m_baseDir.clear();
|
||||
}
|
||||
m_chapters.clear();
|
||||
m_textLengths.clear();
|
||||
m_title.clear();
|
||||
m_author.clear();
|
||||
}
|
||||
@@ -185,6 +262,8 @@ bool EpubBook::open(const QString &epubPath)
|
||||
if (!parseOpf())
|
||||
return false;
|
||||
const bool ok = !m_chapters.isEmpty();
|
||||
if (ok)
|
||||
computeTextLengths();
|
||||
emit changed();
|
||||
return ok;
|
||||
}
|
||||
@@ -418,8 +497,12 @@ QString EpubBook::chapterHtml(int index, int fontSizePx, int marginPx, int margi
|
||||
// stile di reflow iniettato nel documento: !important vince sui CSS del libro.
|
||||
// I margini si possono aggiornare a caldo via runJavaScript (setProperty con
|
||||
// 'important' inline), senza ricaricare il capitolo.
|
||||
// height:auto + overflow:visible forzano lo scroll anche quando il CSS del
|
||||
// libro imposta height:100%/overflow:hidden (senza, il documento non scorre
|
||||
// e ogni tap salta al capitolo successivo).
|
||||
const QString style = QStringLiteral(
|
||||
"<style id=\"cw-font\">html,body{font-size:%1px!important;"
|
||||
"height:auto!important;overflow:visible!important;"
|
||||
"margin-left:%2px!important;margin-right:%2px!important;"
|
||||
"margin-top:%3px!important;margin-bottom:%3px!important}"
|
||||
"p,div,li,td,blockquote,section{font-size:1em!important}</style>")
|
||||
@@ -441,24 +524,84 @@ QString EpubBook::chapterBaseUrl(int index) const
|
||||
.toString();
|
||||
}
|
||||
|
||||
QString EpubBook::makeBookmarkKey(int chapterIndex, double fraction) const
|
||||
void EpubBook::computeTextLengths()
|
||||
{
|
||||
return QStringLiteral("cw:v1:%1:%2").arg(chapterIndex).arg(fraction, 0, 'f', 4);
|
||||
m_textLengths.clear();
|
||||
for (const EpubChapter &c : m_chapters) {
|
||||
QFile f(c.path);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
m_textLengths.append(0);
|
||||
continue;
|
||||
}
|
||||
const QString html = QString::fromUtf8(f.readAll());
|
||||
m_textLengths.append(plainTextLength(html));
|
||||
}
|
||||
}
|
||||
|
||||
int EpubBook::chapterTextLength(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_textLengths.size())
|
||||
return 0;
|
||||
return m_textLengths.at(index);
|
||||
}
|
||||
|
||||
qint64 EpubBook::chapterStartChar(int index) const
|
||||
{
|
||||
qint64 start = 0;
|
||||
const int end = qMin(index, m_textLengths.size());
|
||||
for (int i = 0; i < end; ++i)
|
||||
start += m_textLengths.at(i);
|
||||
return start;
|
||||
}
|
||||
|
||||
QString EpubBook::makeBookmarkKey(int chapterIndex, qint64 charOffset) const
|
||||
{
|
||||
const qint64 abs = chapterStartChar(chapterIndex) + qMax<qint64>(0, charOffset);
|
||||
return QStringLiteral("cw:v2:%1").arg(abs);
|
||||
}
|
||||
|
||||
QVariantMap EpubBook::parseBookmarkKey(const QString &key) const
|
||||
{
|
||||
QVariantMap result;
|
||||
const QStringList parts = key.split(QLatin1Char(':'));
|
||||
if (parts.size() != 4 || parts.at(0) != QLatin1String("cw")
|
||||
|| parts.at(1) != QLatin1String("v1"))
|
||||
if (parts.size() < 3 || parts.at(0) != QLatin1String("cw"))
|
||||
return result;
|
||||
bool okChapter = false, okFrac = false;
|
||||
const int ch = parts.at(2).toInt(&okChapter);
|
||||
const double fr = parts.at(3).toDouble(&okFrac);
|
||||
if (!okChapter || !okFrac)
|
||||
|
||||
if (parts.at(1) == QLatin1String("v2")) {
|
||||
bool ok = false;
|
||||
const qint64 abs = parts.at(2).toLongLong(&ok);
|
||||
if (!ok || abs < 0)
|
||||
return result;
|
||||
// trova il capitolo che contiene il carattere assoluto
|
||||
int ch = 0;
|
||||
qint64 start = 0;
|
||||
for (int i = 0; i < m_textLengths.size(); ++i) {
|
||||
if (start + m_textLengths.at(i) > abs) {
|
||||
ch = i;
|
||||
break;
|
||||
}
|
||||
start += m_textLengths.at(i);
|
||||
ch = i + 1;
|
||||
}
|
||||
if (ch >= m_chapters.size())
|
||||
ch = qMax(0, m_chapters.size() - 1);
|
||||
result.insert(QStringLiteral("chapter"), ch);
|
||||
result.insert(QStringLiteral("charOffset"),
|
||||
qMax<qint64>(0, abs - chapterStartChar(ch)));
|
||||
return result;
|
||||
result.insert(QStringLiteral("chapter"), ch);
|
||||
result.insert(QStringLiteral("fraction"), fr);
|
||||
}
|
||||
|
||||
// v1 (vecchie chiavi, migrazione): "cw:v1:<cap>:<frac>"
|
||||
if (parts.at(1) == QLatin1String("v1") && parts.size() == 4) {
|
||||
bool okC = false, okF = false;
|
||||
const int ch = parts.at(2).toInt(&okC);
|
||||
const double fr = parts.at(3).toDouble(&okF);
|
||||
if (!okC || !okF)
|
||||
return result;
|
||||
result.insert(QStringLiteral("chapter"), qMax(0, ch));
|
||||
result.insert(QStringLiteral("charOffset"),
|
||||
qRound64(fr * chapterTextLength(ch)));
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user