/****************************************************************************
**
** Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies).
** Contact: Qt Software Information (qt-info@nokia.com)
**
** This file is part of the Qt Assistant of the Qt Toolkit.
**
** Commercial Usage
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
**
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License versions 2.0 or 3.0 as published by the Free
** Software Foundation and appearing in the file LICENSE.GPL included in
** the packaging of this file. Please review the following information
** to ensure GNU General Public Licensing requirements will be met:
** http://www.fsf.org/licensing/licenses/info/GPLv2.html and
** http://www.gnu.org/copyleft/gpl.html. In addition, as a special
** exception, Nokia gives you certain additional rights. These rights
** are described in the Nokia Qt GPL Exception version 1.3, included in
** the file GPL_EXCEPTION.txt in this package.
**
** Qt for Windows(R) Licensees
** As a special exception, Nokia, as the sole copyright holder for Qt
** Designer, grants users of the Qt/Eclipse Integration plug-in the
** right for the Qt/Eclipse Integration to link to functionality
** provided by Qt Designer and its related libraries.
**
** If you are unsure which license is appropriate for your use, please
** contact the sales department at qt-sales@nokia.com.
**
****************************************************************************/
#include "HelpIndex.h"
#include <QFile>
#include <QDir>
#include <QStringList>
#include <QApplication>
#include <QByteArray>
#include <QTextStream>
#include <QUrl>
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
#include <QTextCodec>
#else
#include <QStringConverter>
#endif
#include <cctype>
#include <QTextDocument>
#include <QTimer>
#include <algorithm>
#include <utility>
QT_BEGIN_NAMESPACE
struct Term
{
Term() = default;
Term(QString t, int f, QVector<Document> l)
: term(std::move(t))
, frequency(f)
, documents(std::move(l))
{
}
QString term;
int frequency = -1;
QVector<Document> documents;
bool operator<(const Term & i2) const { return frequency < i2.frequency; }
};
QDataStream & operator>>(QDataStream & s, Document & l)
{
s >> l.docNumber;
s >> l.frequency;
return s;
}
QDataStream & operator<<(QDataStream & s, const Document & l)
{
s << (qint16)l.docNumber;
s << (qint16)l.frequency;
return s;
}
HelpIndex::HelpIndex(QString dp, const QString & /* hp */)
: QObject(nullptr)
, docPath(std::move(dp))
{
alreadyHaveDocList = false;
connect(qApp, SIGNAL(lastWindowClosed()), this, SLOT(setLastWinClosed()));
m_pTimer = new QTimer(this);
m_pTimer->setSingleShot(true);
m_pTimer->setInterval(0);
connect(m_pTimer, SIGNAL(timeout()), this, SLOT(filterNext()));
}
HelpIndex::HelpIndex(QStringList dl, const QString & /* hp */)
: QObject(nullptr)
, docList{ std::move(dl) }
{
alreadyHaveDocList = true;
connect(qApp, SIGNAL(lastWindowClosed()), this, SLOT(setLastWinClosed()));
}
void HelpIndex::setLastWinClosed()
{
lastWindowClosed = true;
}
void HelpIndex::setDictionaryFile(const QString & f)
{
dictFile = f;
}
void HelpIndex::setDocListFile(const QString & f)
{
docListFile = f;
}
void HelpIndex::setDocList(const QStringList & lst)
{
docList = lst;
}
void HelpIndex::makeIndex()
{
if(!alreadyHaveDocList)
setupDocumentList();
lastWindowClosed = false;
emit indexingStart(docList.count());
dict.clear();
m_iCurItem = 0;
m_pTimer->start(); //singleshot
}
void HelpIndex::filterNext()
{
if(m_iCurItem < docList.count() && !lastWindowClosed)
{
QUrl url(docList.at(m_iCurItem));
parseDocument(url.toLocalFile(), m_iCurItem);
emit indexingProgress(m_iCurItem);
m_iCurItem++;
m_pTimer->start(); //singleshot
}
else
{
emit indexingEnd();
}
}
void HelpIndex::setupDocumentList()
{
docList.clear();
titleList.clear();
QDir d(docPath);
QStringList filters{QLatin1String("*.html")};
QStringList lst = d.entryList(filters);
for(auto&& item : lst)
{
QString filename = QLatin1String("file:///") + docPath + QLatin1String("/") + item;
docList.append(filename);
titleList.append(getDocumentTitle(filename));
}
}
void HelpIndex::insertInDict(const QString & str, int docNum)
{
if(str == QLatin1String("amp") || str == QLatin1String("nbsp"))
return;
Entry * e = nullptr;
if(dict.count())
e = dict[str];
if(e)
{
if(e->documents.last().docNumber != docNum)
e->documents.append(Document(docNum, 1));
else
e->documents.last().frequency++;
}
else
{
dict.insert(str, new Entry(docNum));
}
}
void HelpIndex::parseDocument(const QString & filename, int docNum)
{
QFile file(filename);
if(!file.open(QFile::ReadOnly))
{
qWarning("Can't open file %s", qPrintable(filename));
return;
}
QTextStream s(&file);
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
s.setCodec(QTextCodec::codecForMib(106));
#else
s.setEncoding(QStringConverter::Utf8);
#endif
QString text = s.readAll();
if(text.isNull())
return;
bool valid = true;
const QChar * buf = text.unicode();
QChar str[64];
QChar c = buf[0];
int j = 0;
int i = 0;
while(j < text.length())
{
if(c == QLatin1Char('<') || c == QLatin1Char('&'))
{
valid = false;
if(i > 1)
insertInDict(QString(str, i), docNum);
i = 0;
c = buf[++j];
continue;
}
if((c == QLatin1Char('>') || c == QLatin1Char(';')) && !valid)
{
valid = true;
c = buf[++j];
continue;
}
if(!valid)
{
c = buf[++j];
continue;
}
if((c.isLetterOrNumber() || c == QLatin1Char('_')) && i < 63)
{
str[i] = c.toLower();
++i;
}
else
{
if(i > 1)
insertInDict(QString(str, i), docNum);
i = 0;
}
c = buf[++j];
}
if(i > 1)
insertInDict(QString(str, i), docNum);
file.close();
}
void HelpIndex::writeDict()
{
QFile f(dictFile);
qDebug("Write dict to %s", f.fileName().toUtf8().data());
if(!f.open(QFile::WriteOnly))
return;
QDataStream s(&f);
for(auto it = dict.cbegin(); it != dict.cend(); ++it)
{
s << it.key();
s << it.value()->documents.count();
s << it.value()->documents;
}
f.close();
writeDocumentList();
}
void HelpIndex::writeDocumentList()
{
QFile f(docListFile);
if(!f.open(QFile::WriteOnly))
return;
QDataStream s(&f);
s << docList;
QFile f1(docListFile + ".titles");
if(!f1.open(QFile::WriteOnly))
return;
QDataStream s1(&f1);
s1 << titleList;
}
void HelpIndex::readDict()
{
QFile f(dictFile);
if(!f.open(QFile::ReadOnly))
return;
dict.clear();
QDataStream s(&f);
QString key;
int numOfDocs;
QVector<Document> docs;
while(!s.atEnd())
{
s >> key;
s >> numOfDocs;
docs.resize(numOfDocs);
s >> docs;
dict.insert(key, new Entry(docs));
}
f.close();
readDocumentList();
}
void HelpIndex::readDocumentList()
{
QFile f(docListFile);
if(!f.open(QFile::ReadOnly))
return;
QDataStream s(&f);
s >> docList;
QFile f1(docListFile + ".titles");
if(!f1.open(QFile::ReadOnly))
return;
QDataStream s1(&f1);
s1 >> titleList;
}
QStringList HelpIndex::query(const QStringList & terms, const QStringList & termSeq, const QStringList & seqWords)
{
QList<Term> termList;
for(const auto & term : terms)
{
if(term.contains(QLatin1Char('*')))
{
QVector<Document> wcts = setupDummyTerm(getWildcardTerms(term));
termList.append(Term(QLatin1String("dummy"), wcts.count(), wcts));
}
else if(dict[term])
{
auto e = dict[term];
termList.append(Term(term, e->documents.count(), e->documents));
}
else
{
return QStringList();
}
}
if(!termList.count())
return QStringList();
std::sort(termList.begin(), termList.end());
QVector<Document> minDocs = termList.takeFirst().documents;
for(const auto & term : termList)
{
QVector<Document> docs = term.documents;
for(auto minDoc_it = minDocs.begin(); minDoc_it != minDocs.end();)
{
bool found = false;
for(auto&& doc : docs)
{
if(minDoc_it->docNumber == doc.docNumber)
{
minDoc_it->frequency += doc.frequency;
found = true;
break;
}
}
if(!found)
minDoc_it = minDocs.erase(minDoc_it);
else
++minDoc_it;
}
}
QStringList results;
std::sort(minDocs.begin(), minDocs.end());
if(termSeq.isEmpty())
{
for(auto & minDoc : minDocs)
results << docList.at((int)minDoc.docNumber);
return results;
}
for(const auto & minDoc : minDocs)
{
auto fileName = docList[(int)minDoc.docNumber];
if(searchForPattern(termSeq, seqWords, fileName))
results << fileName;
}
return results;
}
QString HelpIndex::getDocumentTitle(const QString & fullFileName)
{
QUrl url(fullFileName);
QString fileName = url.toLocalFile();
if(documentTitleCache.contains(fileName))
return documentTitleCache.value(fileName);
QFile file(fileName);
if(!file.open(QFile::ReadOnly))
{
qWarning("Can't open file %s", qPrintable(fileName));
return fileName;
}
QTextStream s(&file);
QString text = s.readAll();
int start = text.indexOf(QLatin1String("<title>"), 0, Qt::CaseInsensitive) + 7;
int end = text.indexOf(QLatin1String("</title>"), start, Qt::CaseInsensitive);
QString title = tr("Untitled");
if(end > start)
{
title = text.mid(start, end - start);
if(Qt::mightBeRichText(title))
{
QTextDocument doc;
doc.setHtml(title);
title = doc.toPlainText();
}
}
documentTitleCache.insert(fileName, title);
return title;
}
QStringList HelpIndex::getWildcardTerms(const QString & term)
{
QStringList lst;
QStringList terms = split(term);
for(auto it = dict.begin(); it != dict.end(); ++it)
{
int index = 0;
bool found = false;
QString text(it.key());
for(auto iter = terms.cbegin(); iter != terms.cend(); ++iter)
{
if(*iter == QLatin1String("*"))
{
found = true;
continue;
}
if(iter == terms.cbegin() && (*iter)[0] != text[0])
{
found = false;
break;
}
index = text.indexOf(*iter, index);
if(*iter == terms.last() && index != text.length() - 1)
{
index = text.lastIndexOf(*iter);
if(index != text.length() - iter->length())
{
found = false;
break;
}
}
if(index != -1)
{
found = true;
index += iter->length();
continue;
}
else
{
found = false;
break;
}
}
if(found)
lst << text;
}
return lst;
}
QStringList HelpIndex::split(const QString & str)
{
QStringList lst;
int j = 0;
int i = str.indexOf(QLatin1Char('*'), j);
if(str.startsWith(QLatin1String("*")))
lst << QLatin1String("*");
while(i != -1)
{
if(i > j && i <= (int)str.length())
{
lst << str.mid(j, i - j);
lst << QLatin1String("*");
}
j = i + 1;
i = str.indexOf(QLatin1Char('*'), j);
}
int l = str.length() - 1;
if(str.mid(j, l - j + 1).length() > 0)
lst << str.mid(j, l - j + 1);
return lst;
}
QVector<Document> HelpIndex::setupDummyTerm(const QStringList & terms)
{
QList<Term> termList;
for(const auto & term : terms)
{
if(dict[term])
{
auto e = dict[term];
termList.append(Term(term, e->documents.count(), e->documents));
}
}
if(!termList.count())
return QVector<Document>();
std::sort(termList.begin(), termList.end());
auto maxList = termList.takeLast().documents;
for(const auto & term : termList)
for(const auto & doc : term.documents)
if(maxList.indexOf(doc) == -1)
maxList.append(doc);
return maxList;
}
void HelpIndex::buildMiniDict(const QString & str)
{
if(miniDict[str])
miniDict[str]->positions.append(wordNum);
++wordNum;
}
bool HelpIndex::searchForPattern(const QStringList & patterns, const QStringList & words, const QString & fileName)
{
QUrl url(fileName);
QString fName = url.toLocalFile();
QFile file(fName);
if(!file.open(QFile::ReadOnly))
{
qWarning("Can't open file %s", qPrintable(fName));
return false;
}
wordNum = 3;
miniDict.clear();
for(auto&& word : words)
miniDict.insert(word, new PosEntry(0));
QTextStream s(&file);
QString text = s.readAll();
bool valid = true;
const QChar * buf = text.unicode();
QChar str[64];
QChar c = buf[0];
int j = 0;
int i = 0;
while(j < text.length())
{
if(c == QLatin1Char('<') || c == QLatin1Char('&'))
{
valid = false;
if(i > 1)
buildMiniDict(QString(str, i));
i = 0;
c = buf[++j];
continue;
}
if((c == QLatin1Char('>') || c == QLatin1Char(';')) && !valid)
{
valid = true;
c = buf[++j];
continue;
}
if(!valid)
{
c = buf[++j];
continue;
}
if((c.isLetterOrNumber() || c == QLatin1Char('_')) && i < 63)
{
str[i] = c.toLower();
++i;
}
else
{
if(i > 1)
buildMiniDict(QString(str, i));
i = 0;
}
c = buf[++j];
}
if(i > 1)
buildMiniDict(QString(str, i));
file.close();
QStringList wordLst;
QList<uint> a;
for(auto&& pattern : patterns)
{
wordLst = pattern.split(QLatin1Char(' '));
a = miniDict[wordLst[0]]->positions;
for(int j = 1; j < (int)wordLst.count(); ++j)
{
auto b = miniDict[wordLst[j]]->positions;
auto aIt = a.begin();
while(aIt != a.end())
{
if(b.contains(*aIt + 1))
{
(*aIt)++;
++aIt;
}
else
{
aIt = a.erase(aIt);
}
}
}
}
if(a.count())
return true;
return false;
}
QT_END_NAMESPACE