Lettore 0.4.0: rifatto senza WebView (Text rich text QML)
Il WebView di Sailfish non supporta lo scroll programmatico affidabile (window.scrollY stale, contentHeight/scrollPosition non scrivibili): 0.3.x non ha mai cambiato capitolo in modo affidabile. Ora i capitoli sono rich text Qt in un Text QML dentro un SilicaFlickable: contentY/contentHeight nativi, paginazione a tap deterministico, fine capitolo esatta. HTML->rich text in C++ (tag base, entità, fallback). Bookmark v2 invariato (offset caratteri stimato da frazione). Niente immagini inline (limite rich text).
This commit is contained in:
+239
@@ -193,6 +193,234 @@ int plainTextLength(const QString &html)
|
||||
return count;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HTML -> rich text Qt (per Text QML, niente WebView): tag di base convertiti,
|
||||
// blocchi raw (script/style/head/...) saltati, immagini ignorate, i tag non
|
||||
// supportati diventano trasparenti (resta il contenuto).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Entità HTML nominate NON standard XML (QXmlStreamReader le rifiuta senza DTD):
|
||||
// decodificate prima del parse. Quelle XML standard (amp lt gt quot apos) e le
|
||||
// numeriche (&#NNN; &#xHH;) le gestisce il parser.
|
||||
QString decodeHtmlEntities(const QString &s)
|
||||
{
|
||||
static const QHash<QString, QChar> entities = [] {
|
||||
QHash<QString, QChar> h;
|
||||
h.insert(QStringLiteral("nbsp"), QChar(0x00A0));
|
||||
h.insert(QStringLiteral("mdash"), QChar(0x2014));
|
||||
h.insert(QStringLiteral("ndash"), QChar(0x2013));
|
||||
h.insert(QStringLiteral("rsquo"), QChar(0x2019));
|
||||
h.insert(QStringLiteral("lsquo"), QChar(0x2018));
|
||||
h.insert(QStringLiteral("rdquo"), QChar(0x201D));
|
||||
h.insert(QStringLiteral("ldquo"), QChar(0x201C));
|
||||
h.insert(QStringLiteral("sbquo"), QChar(0x201A));
|
||||
h.insert(QStringLiteral("bdquo"), QChar(0x201E));
|
||||
h.insert(QStringLiteral("hellip"), QChar(0x2026));
|
||||
h.insert(QStringLiteral("dagger"), QChar(0x2020));
|
||||
h.insert(QStringLiteral("Dagger"), QChar(0x2021));
|
||||
h.insert(QStringLiteral("bull"), QChar(0x2022));
|
||||
h.insert(QStringLiteral("middot"), QChar(0x00B7));
|
||||
h.insert(QStringLiteral("eacute"), QChar(0x00E9));
|
||||
h.insert(QStringLiteral("egrave"), QChar(0x00E8));
|
||||
h.insert(QStringLiteral("agrave"), QChar(0x00E0));
|
||||
h.insert(QStringLiteral("ugrave"), QChar(0x00F9));
|
||||
h.insert(QStringLiteral("igrave"), QChar(0x00EC));
|
||||
h.insert(QStringLiteral("ograve"), QChar(0x00F2));
|
||||
h.insert(QStringLiteral("aacute"), QChar(0x00E1));
|
||||
h.insert(QStringLiteral("uacute"), QChar(0x00FA));
|
||||
h.insert(QStringLiteral("iacute"), QChar(0x00ED));
|
||||
h.insert(QStringLiteral("oacute"), QChar(0x00F3));
|
||||
h.insert(QStringLiteral("auml"), QChar(0x00E4));
|
||||
h.insert(QStringLiteral("euml"), QChar(0x00EB));
|
||||
h.insert(QStringLiteral("iuml"), QChar(0x00EF));
|
||||
h.insert(QStringLiteral("ouml"), QChar(0x00F6));
|
||||
h.insert(QStringLiteral("uuml"), QChar(0x00FC));
|
||||
h.insert(QStringLiteral("szlig"), QChar(0x00DF));
|
||||
h.insert(QStringLiteral("deg"), QChar(0x00B0));
|
||||
h.insert(QStringLiteral("plusmn"), QChar(0x00B1));
|
||||
h.insert(QStringLiteral("times"), QChar(0x00D7));
|
||||
h.insert(QStringLiteral("divide"), QChar(0x00F7));
|
||||
h.insert(QStringLiteral("copy"), QChar(0x00A9));
|
||||
h.insert(QStringLiteral("reg"), QChar(0x00AE));
|
||||
h.insert(QStringLiteral("trade"), QChar(0x2122));
|
||||
h.insert(QStringLiteral("euro"), QChar(0x20AC));
|
||||
h.insert(QStringLiteral("pound"), QChar(0x00A3));
|
||||
h.insert(QStringLiteral("cent"), QChar(0x00A2));
|
||||
h.insert(QStringLiteral("yen"), QChar(0x00A5));
|
||||
h.insert(QStringLiteral("para"), QChar(0x00B6));
|
||||
h.insert(QStringLiteral("sect"), QChar(0x00A7));
|
||||
h.insert(QStringLiteral("laquo"), QChar(0x00AB));
|
||||
h.insert(QStringLiteral("raquo"), QChar(0x00BB));
|
||||
h.insert(QStringLiteral("lsaquo"), QChar(0x2039));
|
||||
h.insert(QStringLiteral("rsaquo"), QChar(0x203A));
|
||||
h.insert(QStringLiteral("prime"), QChar(0x2032));
|
||||
h.insert(QStringLiteral("Prime"), QChar(0x2033));
|
||||
h.insert(QStringLiteral("permil"), QChar(0x2030));
|
||||
return h;
|
||||
}();
|
||||
|
||||
QString out;
|
||||
out.reserve(s.size());
|
||||
int i = 0;
|
||||
const int n = s.size();
|
||||
while (i < n) {
|
||||
const QChar c = s.at(i);
|
||||
if (c == QLatin1Char('&')) {
|
||||
const int semi = s.indexOf(QLatin1Char(';'), i + 1);
|
||||
if (semi > i && semi - i <= 12) {
|
||||
const QString ent = s.mid(i + 1, semi - i - 1);
|
||||
const QChar ch = entities.value(ent);
|
||||
if (!ch.isNull()) {
|
||||
out += ch;
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
out += c;
|
||||
++i;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString htmlToRichText(const QString &html)
|
||||
{
|
||||
const int bodyStart = html.indexOf(QStringLiteral("<body"), 0, Qt::CaseInsensitive);
|
||||
const int bodyEnd = html.indexOf(QStringLiteral("</body"), 0, Qt::CaseInsensitive);
|
||||
QString scope;
|
||||
if (bodyStart >= 0) {
|
||||
// includi anche la chiusura </body>: il frammento deve essere XML valido
|
||||
const int end = (bodyEnd >= bodyStart) ? bodyEnd + 7 : html.size();
|
||||
scope = html.mid(bodyStart, end - bodyStart);
|
||||
} else {
|
||||
scope = html;
|
||||
}
|
||||
scope = decodeHtmlEntities(scope);
|
||||
|
||||
QString out;
|
||||
QStringList stack;
|
||||
QXmlStreamReader r(scope);
|
||||
while (!r.atEnd() && !r.hasError()) {
|
||||
const QXmlStreamReader::TokenType t = r.readNext();
|
||||
if (t == QXmlStreamReader::StartElement) {
|
||||
const QString name = r.name().toString().toLower();
|
||||
if (name == QLatin1String("script") || name == QLatin1String("style")
|
||||
|| name == QLatin1String("noscript") || name == QLatin1String("head")
|
||||
|| name == QLatin1String("title")) {
|
||||
r.skipCurrentElement();
|
||||
continue;
|
||||
}
|
||||
if (name == QLatin1String("br")) {
|
||||
out += QStringLiteral("<br>");
|
||||
stack.append(QString());
|
||||
continue;
|
||||
}
|
||||
if (name == QLatin1String("img") || name == QLatin1String("hr")
|
||||
|| name == QLatin1String("input")) {
|
||||
stack.append(QString());
|
||||
continue;
|
||||
}
|
||||
QString tag;
|
||||
if (name == QLatin1String("b") || name == QLatin1String("strong"))
|
||||
tag = QStringLiteral("b");
|
||||
else if (name == QLatin1String("i") || name == QLatin1String("em"))
|
||||
tag = QStringLiteral("i");
|
||||
else if (name == QLatin1String("u"))
|
||||
tag = QStringLiteral("u");
|
||||
else if (name == QLatin1String("s") || name == QLatin1String("strike")
|
||||
|| name == QLatin1String("del"))
|
||||
tag = QStringLiteral("s");
|
||||
else if (name == QLatin1String("ul"))
|
||||
tag = QStringLiteral("ul");
|
||||
else if (name == QLatin1String("ol"))
|
||||
tag = QStringLiteral("ol");
|
||||
else if (name == QLatin1String("li"))
|
||||
tag = QStringLiteral("li");
|
||||
else if (name == QLatin1String("table"))
|
||||
tag = QStringLiteral("table border=\"1\" cellspacing=\"0\" cellpadding=\"4\"");
|
||||
else if (name == QLatin1String("tr"))
|
||||
tag = QStringLiteral("tr");
|
||||
else if (name == QLatin1String("td") || name == QLatin1String("th"))
|
||||
tag = QStringLiteral("td");
|
||||
else if (name == QLatin1String("p") || name == QLatin1String("blockquote"))
|
||||
tag = QStringLiteral("p");
|
||||
else if (name == QLatin1String("h1") || name == QLatin1String("h2")
|
||||
|| name == QLatin1String("h3") || name == QLatin1String("h4")
|
||||
|| name == QLatin1String("h5") || name == QLatin1String("h6"))
|
||||
tag = name;
|
||||
// div/span/a/section/... -> trasparenti (tag vuoto)
|
||||
if (!tag.isEmpty()) {
|
||||
out += QLatin1Char('<') + tag + QLatin1Char('>');
|
||||
stack.append(tag);
|
||||
} else {
|
||||
stack.append(QString());
|
||||
}
|
||||
} else if (t == QXmlStreamReader::EndElement) {
|
||||
if (!stack.isEmpty()) {
|
||||
const QString tag = stack.takeLast();
|
||||
if (!tag.isEmpty()) {
|
||||
const int sp = tag.indexOf(QLatin1Char(' '));
|
||||
out += QStringLiteral("</") + (sp > 0 ? tag.left(sp) : tag) + QLatin1Char('>');
|
||||
}
|
||||
}
|
||||
} else if (t == QXmlStreamReader::Characters) {
|
||||
out += r.text();
|
||||
}
|
||||
}
|
||||
|
||||
// HTML non ben formato (parse fallito): fallback grezzo — testo con <br>
|
||||
// sui newline e paragrafi sui doppi newline
|
||||
if (r.hasError()) {
|
||||
out.clear();
|
||||
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);
|
||||
i = (close < 0) ? n : 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")) out += QLatin1Char('&');
|
||||
else if (ent == QLatin1String("lt")) out += QLatin1Char('<');
|
||||
else if (ent == QLatin1String("gt")) out += QLatin1Char('>');
|
||||
else if (ent == QLatin1String("nbsp")) out += QLatin1Char(' ');
|
||||
else if (ent.startsWith(QLatin1String("#"))) {
|
||||
bool ok = false;
|
||||
const int cp = ent.mid(1).toInt(&ok);
|
||||
if (ok && cp > 0 && cp < 0x10FFFF)
|
||||
out += QChar(cp);
|
||||
} else {
|
||||
out += c;
|
||||
out += ent;
|
||||
out += QLatin1Char(';');
|
||||
}
|
||||
i = semi + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (c == QLatin1Char('\n')) {
|
||||
// doppio newline = paragrafo, singolo = a capo
|
||||
if (i + 1 < n && scope.at(i + 1) == QLatin1Char('\n')) {
|
||||
out += QStringLiteral("</p><p>");
|
||||
++i;
|
||||
} else {
|
||||
out += QStringLiteral("<br>");
|
||||
}
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
out += c;
|
||||
++i;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -524,6 +752,17 @@ QString EpubBook::chapterBaseUrl(int index) const
|
||||
.toString();
|
||||
}
|
||||
|
||||
QString EpubBook::chapterRichText(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_chapters.size())
|
||||
return QString();
|
||||
QFile f(m_chapters.at(index).path);
|
||||
if (!f.open(QIODevice::ReadOnly))
|
||||
return QString();
|
||||
const QString html = QString::fromUtf8(f.readAll());
|
||||
return htmlToRichText(html);
|
||||
}
|
||||
|
||||
void EpubBook::computeTextLengths()
|
||||
{
|
||||
m_textLengths.clear();
|
||||
|
||||
Reference in New Issue
Block a user