7#include <qhelp_global.h>
9#include <QtCore/QtMath>
11#include <QtCore/QFile>
12#include <QtCore/QFileInfo>
14#include <QtCore/QDebug>
15#include <QtCore/QRegularExpression>
18#include <QtCore/QVariant>
19#include <QtCore/QDateTime>
20#include <QtCore/QStringConverter>
21#include <QtCore/QDataStream>
22#include <QtSql/QSqlQuery>
28class HelpGeneratorPrivate :
public QObject
33 HelpGeneratorPrivate(QObject *parent =
nullptr) : QObject(parent) {}
35 bool generate(QHelpProjectData *helpData,
36 const QString &outputFileName);
37 bool checkLinks(
const QHelpProjectData &helpData);
38 QString error()
const;
41 void statusChanged(
const QString &msg);
42 void progressChanged(
double progress);
43 void warning(
const QString &msg);
46 struct FileNameTableData
53 void writeTree(QDataStream &s, QHelpDataContentItem *item,
int depth);
55 bool insertFileNotFoundFile();
56 bool registerCustomFilter(
const QString &filterName,
57 const QStringList &filterAttribs,
bool forceUpdate =
false);
58 bool registerVirtualFolder(
const QString &folderName,
const QString &ns);
59 bool insertFilterAttributes(
const QStringList &attributes);
60 bool insertKeywords(
const QList<QHelpDataIndexItem> &keywords,
61 const QStringList &filterAttributes);
62 bool insertFiles(
const QStringList &files,
const QString &rootPath,
63 const QStringList &filterAttributes);
64 bool insertContents(
const QByteArray &ba,
65 const QStringList &filterAttributes);
66 bool insertMetaData(
const QMap<QString, QVariant> &metaData);
68 void setupProgress(QHelpProjectData *helpData);
69 void addProgress(
double step);
72 QSqlQuery *m_query =
nullptr;
74 int m_namespaceId = -1;
75 int m_virtualFolderId = -1;
77 QMap<QString,
int> m_fileMap;
78 QMap<
int, QSet<
int> > m_fileFilterMap;
88
89
90
91
93 const QString &outputFileName)
95 emit progressChanged(0);
97 if (!helpData || helpData->namespaceName().isEmpty()) {
98 m_error = tr(
"Invalid help data.");
102 QString outFileName = outputFileName;
103 if (outFileName.isEmpty()) {
104 m_error = tr(
"No output file name specified.");
108 QFileInfo fi(outFileName);
110 if (!fi.dir().remove(fi.fileName())) {
111 m_error = tr(
"The file %1 cannot be overwritten.").arg(outFileName);
116 setupProgress(helpData);
118 emit statusChanged(tr(
"Building up file structure..."));
119 bool openingOk =
true;
121 QSqlDatabase db = QSqlDatabase::addDatabase(QLatin1String(
"QSQLITE"), QLatin1String(
"builder"));
122 db.setDatabaseName(outFileName);
123 openingOk = db.open();
125 m_query =
new QSqlQuery(db);
129 m_error = tr(
"Cannot open data base file %1.").arg(outFileName);
134 m_query->exec(QLatin1String(
"PRAGMA synchronous=OFF"));
135 m_query->exec(QLatin1String(
"PRAGMA cache_size=3000"));
139 insertFileNotFoundFile();
140 insertMetaData(helpData->metaData());
142 if (!registerVirtualFolder(helpData->virtualFolder(), helpData->namespaceName())) {
143 m_error = tr(
"Cannot register namespace \"%1\".").arg(helpData->namespaceName());
149 emit statusChanged(tr(
"Insert custom filters..."));
150 for (
const QHelpDataCustomFilter &f : helpData->customFilters()) {
151 if (!registerCustomFilter(f.name, f.filterAttributes,
true)) {
159 for (
const QHelpDataFilterSection &fs : helpData->filterSections()) {
160 emit statusChanged(tr(
"Insert help data for filter section (%1 of %2)...")
161 .arg(i++).arg(helpData->filterSections().size()));
162 insertFilterAttributes(fs.filterAttributes());
164 QDataStream s(&ba, QIODevice::WriteOnly);
165 for (QHelpDataContentItem *itm : fs.contents())
166 writeTree(s, itm, 0);
167 if (!insertFiles(fs.files(), helpData->rootPath(), fs.filterAttributes())
168 || !insertContents(ba, fs.filterAttributes())
169 || !insertKeywords(fs.indices(), fs.filterAttributes())) {
176 emit progressChanged(100);
177 emit statusChanged(tr(
"Documentation successfully generated."));
186 int numberOfFiles = 0;
187 int numberOfIndices = 0;
188 for (
const QHelpDataFilterSection &fs : helpData->filterSections()) {
189 numberOfFiles += fs.files().size();
190 numberOfIndices += fs.indices().size();
197 m_contentStep = 10.0 / qMax(helpData->customFilters().size(), 1);
198 m_fileStep = 60.0 / qMax(numberOfFiles, 1);
199 m_indexStep = 27.0 / qMax(numberOfIndices, 1);
202void HelpGeneratorPrivate::addProgress(
double step)
205 if ((m_progress - m_oldProgress) >= 1.0 && m_progress <= 100.0) {
206 m_oldProgress = m_progress;
207 emit progressChanged(qCeil(m_progress));
211void HelpGeneratorPrivate::cleanupDB()
218 QSqlDatabase::removeDatabase(QLatin1String(
"builder"));
221void HelpGeneratorPrivate::writeTree(QDataStream &s, QHelpDataContentItem *item,
int depth)
224 s << item->reference();
226 for (QHelpDataContentItem *i : item->children())
227 writeTree(s, i, depth + 1);
231
232
233QString HelpGeneratorPrivate::error()
const
238bool HelpGeneratorPrivate::createTables()
243 m_query->exec(QLatin1String(
"SELECT COUNT(*) FROM sqlite_master WHERE TYPE=\'table\'"
244 "AND Name=\'NamespaceTable\'"));
246 if (m_query->value(0).toInt() > 0) {
247 m_error = tr(
"Some tables already exist.");
251 const QStringList tables = QStringList()
252 << QLatin1String(
"CREATE TABLE NamespaceTable ("
253 "Id INTEGER PRIMARY KEY,"
255 << QLatin1String(
"CREATE TABLE FilterAttributeTable ("
256 "Id INTEGER PRIMARY KEY, "
258 << QLatin1String(
"CREATE TABLE FilterNameTable ("
259 "Id INTEGER PRIMARY KEY, "
261 << QLatin1String(
"CREATE TABLE FilterTable ("
263 "FilterAttributeId INTEGER )")
264 << QLatin1String(
"CREATE TABLE IndexTable ("
265 "Id INTEGER PRIMARY KEY, "
268 "NamespaceId INTEGER, "
271 << QLatin1String(
"CREATE TABLE IndexFilterTable ("
272 "FilterAttributeId INTEGER, "
274 << QLatin1String(
"CREATE TABLE ContentsTable ("
275 "Id INTEGER PRIMARY KEY, "
276 "NamespaceId INTEGER, "
278 << QLatin1String(
"CREATE TABLE ContentsFilterTable ("
279 "FilterAttributeId INTEGER, "
280 "ContentsId INTEGER )")
281 << QLatin1String(
"CREATE TABLE FileAttributeSetTable ("
283 "FilterAttributeId INTEGER )")
284 << QLatin1String(
"CREATE TABLE FileDataTable ("
285 "Id INTEGER PRIMARY KEY, "
287 << QLatin1String(
"CREATE TABLE FileFilterTable ("
288 "FilterAttributeId INTEGER, "
290 << QLatin1String(
"CREATE TABLE FileNameTable ("
295 << QLatin1String(
"CREATE TABLE FolderTable("
296 "Id INTEGER PRIMARY KEY, "
298 "NamespaceID INTEGER )")
299 << QLatin1String(
"CREATE TABLE MetaDataTable("
303 for (
const QString &q : tables) {
304 if (!m_query->exec(q)) {
305 m_error = tr(
"Cannot create tables.");
310 m_query->exec(QLatin1String(
"INSERT INTO MetaDataTable VALUES('qchVersion', '1.0')"));
315bool HelpGeneratorPrivate::insertFileNotFoundFile()
320 m_query->exec(QLatin1String(
"SELECT id FROM FileNameTable WHERE Name=\'\'"));
321 if (m_query->next() && m_query->isValid())
324 m_query->prepare(QLatin1String(
"INSERT INTO FileDataTable VALUES (Null, ?)"));
325 m_query->bindValue(0, QByteArray());
326 if (!m_query->exec())
329 const int fileId = m_query->lastInsertId().toInt();
330 m_query->prepare(QLatin1String(
"INSERT INTO FileNameTable (FolderId, Name, FileId, Title) "
331 " VALUES (0, '', ?, '')"));
332 m_query->bindValue(0, fileId);
333 if (fileId > -1 && m_query->exec()) {
334 m_fileMap.insert({}, fileId);
340bool HelpGeneratorPrivate::registerVirtualFolder(
const QString &folderName,
const QString &ns)
342 if (!m_query || folderName.isEmpty() || ns.isEmpty())
345 m_query->prepare(QLatin1String(
"SELECT Id FROM FolderTable WHERE Name=?"));
346 m_query->bindValue(0, folderName);
349 if (m_query->isValid() && m_query->value(0).toInt() > 0)
353 m_query->prepare(QLatin1String(
"SELECT Id FROM NamespaceTable WHERE Name=?"));
354 m_query->bindValue(0, ns);
356 while (m_query->next()) {
357 m_namespaceId = m_query->value(0).toInt();
361 if (m_namespaceId < 0) {
362 m_query->prepare(QLatin1String(
"INSERT INTO NamespaceTable VALUES(NULL, ?)"));
363 m_query->bindValue(0, ns);
365 m_namespaceId = m_query->lastInsertId().toInt();
368 if (m_namespaceId > 0) {
369 m_query->prepare(QLatin1String(
"SELECT Id FROM FolderTable WHERE Name=?"));
370 m_query->bindValue(0, folderName);
372 while (m_query->next())
373 m_virtualFolderId = m_query->value(0).toInt();
375 if (m_virtualFolderId > 0)
378 m_query->prepare(QLatin1String(
"INSERT INTO FolderTable (NamespaceId, Name) "
380 m_query->bindValue(0, m_namespaceId);
381 m_query->bindValue(1, folderName);
382 if (m_query->exec()) {
383 m_virtualFolderId = m_query->lastInsertId().toInt();
384 return m_virtualFolderId > 0;
387 m_error = tr(
"Cannot register virtual folder.");
391bool HelpGeneratorPrivate::insertFiles(
const QStringList &files,
const QString &rootPath,
392 const QStringList &filterAttributes)
397 emit statusChanged(tr(
"Insert files..."));
398 QSet<
int> filterAtts;
399 for (
const QString &filterAtt : filterAttributes) {
400 m_query->prepare(QLatin1String(
"SELECT Id FROM FilterAttributeTable "
402 m_query->bindValue(0, filterAtt);
405 filterAtts.insert(m_query->value(0).toInt());
408 int filterSetId = -1;
409 m_query->exec(QLatin1String(
"SELECT MAX(Id) FROM FileAttributeSetTable"));
411 filterSetId = m_query->value(0).toInt();
415 QList<
int> attValues = filterAtts.values();
416 std::sort(attValues.begin(), attValues.end());
417 for (
int attId : std::as_const(attValues)) {
418 m_query->prepare(QLatin1String(
"INSERT INTO FileAttributeSetTable "
420 m_query->bindValue(0, filterSetId);
421 m_query->bindValue(1, attId);
426 m_query->exec(QLatin1String(
"SELECT MAX(Id) FROM FileDataTable"));
428 tableFileId = m_query->value(0).toInt() + 1;
432 QList<QByteArray> fileDataList;
433 QMap<
int, QSet<
int> > tmpFileFilterMap;
434 QList<FileNameTableData> fileNameDataList;
437 for (
const QString &file : files) {
438 const QString fileName = QDir::cleanPath(file);
440 QFile fi(rootPath + QDir::separator() + fileName);
442 emit warning(tr(
"The file %1 does not exist, skipping it...")
443 .arg(QDir::cleanPath(rootPath + QDir::separator() + fileName)));
447 if (!fi.open(QIODevice::ReadOnly)) {
448 emit warning(tr(
"Cannot open file %1, skipping it...")
449 .arg(QDir::cleanPath(rootPath + QDir::separator() + fileName)));
453 QByteArray data = fi.readAll();
454 if (fileName.endsWith(QLatin1String(
".html"))
455 || fileName.endsWith(QLatin1String(
".htm"))) {
456 auto encoding = QStringDecoder::encodingForHtml(data);
458 encoding = QStringDecoder::Utf8;
459 title = QHelpGlobal::documentTitle(QStringDecoder(*encoding)(data));
461 title = fileName.mid(fileName.lastIndexOf(QLatin1Char(
'/')) + 1);
465 const auto &it = m_fileMap.constFind(fileName);
466 if (it == m_fileMap.cend()) {
467 fileDataList.append(qCompress(data));
469 FileNameTableData fileNameData;
470 fileNameData.name = fileName;
471 fileNameData.fileId = tableFileId;
472 fileNameData.title = title;
473 fileNameDataList.append(fileNameData);
475 m_fileMap.insert(fileName, tableFileId);
476 m_fileFilterMap.insert(tableFileId, filterAtts);
477 tmpFileFilterMap.insert(tableFileId, filterAtts);
482 QSet<
int> &fileFilterSet = m_fileFilterMap[fileId];
483 QSet<
int> &tmpFileFilterSet = tmpFileFilterMap[fileId];
484 for (
int filter : std::as_const(filterAtts)) {
485 if (!fileFilterSet.contains(filter)
486 && !tmpFileFilterSet.contains(filter)) {
487 fileFilterSet.insert(filter);
488 tmpFileFilterSet.insert(filter);
494 if (!tmpFileFilterMap.isEmpty()) {
495 m_query->exec(QLatin1String(
"BEGIN"));
496 for (
auto it = tmpFileFilterMap.cbegin(), end = tmpFileFilterMap.cend(); it != end; ++it) {
497 QList<
int> filterValues = it.value().values();
498 std::sort(filterValues.begin(), filterValues.end());
499 for (
int fv : std::as_const(filterValues)) {
500 m_query->prepare(QLatin1String(
"INSERT INTO FileFilterTable "
502 m_query->bindValue(0, fv);
503 m_query->bindValue(1, it.key());
508 for (
const QByteArray &fileData : std::as_const(fileDataList)) {
509 m_query->prepare(QLatin1String(
"INSERT INTO FileDataTable VALUES "
511 m_query->bindValue(0, fileData);
514 addProgress(m_fileStep * 20.0);
517 for (
const FileNameTableData &fnd : std::as_const(fileNameDataList)) {
518 m_query->prepare(QLatin1String(
"INSERT INTO FileNameTable "
519 "(FolderId, Name, FileId, Title) VALUES (?, ?, ?, ?)"));
520 m_query->bindValue(0, 1);
521 m_query->bindValue(1, fnd.name);
522 m_query->bindValue(2, fnd.fileId);
523 m_query->bindValue(3, fnd.title);
526 m_query->exec(QLatin1String(
"COMMIT"));
529 m_query->exec(QLatin1String(
"SELECT MAX(Id) FROM FileDataTable"));
531 && m_query->value(0).toInt() == tableFileId - 1) {
532 addProgress(m_fileStep*(i % 20));
538bool HelpGeneratorPrivate::registerCustomFilter(
const QString &filterName,
539 const QStringList &filterAttribs,
bool forceUpdate)
544 m_query->exec(QLatin1String(
"SELECT Id, Name FROM FilterAttributeTable"));
545 QStringList idsToInsert = filterAttribs;
546 QMap<QString,
int> attributeMap;
547 while (m_query->next()) {
548 attributeMap.insert(m_query->value(1).toString(),
549 m_query->value(0).toInt());
550 idsToInsert.removeAll(m_query->value(1).toString());
553 for (
const QString &id : std::as_const(idsToInsert)) {
554 m_query->prepare(QLatin1String(
"INSERT INTO FilterAttributeTable VALUES(NULL, ?)"));
555 m_query->bindValue(0, id);
557 attributeMap.insert(id, m_query->lastInsertId().toInt());
561 m_query->prepare(QLatin1String(
"SELECT Id FROM FilterNameTable WHERE Name=?"));
562 m_query->bindValue(0, filterName);
564 while (m_query->next()) {
565 nameId = m_query->value(0).toInt();
570 m_query->prepare(QLatin1String(
"INSERT INTO FilterNameTable VALUES(NULL, ?)"));
571 m_query->bindValue(0, filterName);
573 nameId = m_query->lastInsertId().toInt();
574 }
else if (!forceUpdate) {
575 m_error = tr(
"The filter %1 is already registered.").arg(filterName);
580 m_error = tr(
"Cannot register filter %1.").arg(filterName);
584 m_query->prepare(QLatin1String(
"DELETE FROM FilterTable WHERE NameId=?"));
585 m_query->bindValue(0, nameId);
588 for (
const QString &att : filterAttribs) {
589 m_query->prepare(QLatin1String(
"INSERT INTO FilterTable VALUES(?, ?)"));
590 m_query->bindValue(0, nameId);
591 m_query->bindValue(1, attributeMap[att]);
592 if (!m_query->exec())
598bool HelpGeneratorPrivate::insertKeywords(
const QList<QHelpDataIndexItem> &keywords,
599 const QStringList &filterAttributes)
604 emit statusChanged(tr(
"Insert indices..."));
606 m_query->exec(QLatin1String(
"SELECT MAX(Id) FROM IndexTable"));
608 indexId = m_query->value(0).toInt() + 1;
610 QList<
int> filterAtts;
611 for (
const QString &filterAtt : filterAttributes) {
612 m_query->prepare(QLatin1String(
"SELECT Id FROM FilterAttributeTable WHERE Name=?"));
613 m_query->bindValue(0, filterAtt);
616 filterAtts.append(m_query->value(0).toInt());
619 QList<
int> indexFilterTable;
622 m_query->exec(QLatin1String(
"BEGIN"));
623 QSet<QString> indices;
624 for (
const QHelpDataIndexItem &itm : keywords) {
627 if (indices.contains(itm.identifier))
632 if (!itm.identifier.isEmpty())
633 indices.insert(itm.identifier);
635 const int pos = itm.reference.indexOf(QLatin1Char(
'#'));
636 const QString &fileName = itm.reference.left(pos);
637 const QString anchor = pos < 0 ? QString() : itm.reference.mid(pos + 1);
639 const QString &fName = QDir::cleanPath(fileName);
641 const auto &it = m_fileMap.constFind(fName);
642 const int fileId = it == m_fileMap.cend() ? 1 : it.value();
644 m_query->prepare(QLatin1String(
"INSERT INTO IndexTable (Name, Identifier, NamespaceId, FileId, Anchor) "
645 "VALUES(?, ?, ?, ?, ?)"));
646 m_query->bindValue(0, itm.name);
647 m_query->bindValue(1, itm.identifier);
648 m_query->bindValue(2, m_namespaceId);
649 m_query->bindValue(3, fileId);
650 m_query->bindValue(4, anchor);
653 indexFilterTable.append(indexId++);
655 addProgress(m_indexStep * 100.0);
657 m_query->exec(QLatin1String(
"COMMIT"));
659 m_query->exec(QLatin1String(
"BEGIN"));
660 for (
int idx : std::as_const(indexFilterTable)) {
661 for (
int a : std::as_const(filterAtts)) {
662 m_query->prepare(QLatin1String(
"INSERT INTO IndexFilterTable (FilterAttributeId, IndexId) "
664 m_query->bindValue(0, a);
665 m_query->bindValue(1, idx);
669 m_query->exec(QLatin1String(
"COMMIT"));
671 m_query->exec(QLatin1String(
"SELECT COUNT(Id) FROM IndexTable"));
672 if (m_query->next() && m_query->value(0).toInt() >= indices.size())
677bool HelpGeneratorPrivate::insertContents(
const QByteArray &ba,
678 const QStringList &filterAttributes)
683 emit statusChanged(tr(
"Insert contents..."));
684 m_query->prepare(QLatin1String(
"INSERT INTO ContentsTable (NamespaceId, Data) "
686 m_query->bindValue(0, m_namespaceId);
687 m_query->bindValue(1, ba);
689 int contentId = m_query->lastInsertId().toInt();
691 m_error = tr(
"Cannot insert contents.");
696 for (
const QString &filterAtt : filterAttributes) {
697 m_query->prepare(QLatin1String(
"INSERT INTO ContentsFilterTable (FilterAttributeId, ContentsId) "
698 "SELECT Id, ? FROM FilterAttributeTable WHERE Name=?"));
699 m_query->bindValue(0, contentId);
700 m_query->bindValue(1, filterAtt);
702 if (!m_query->isActive()) {
703 m_error = tr(
"Cannot register contents.");
707 addProgress(m_contentStep);
711bool HelpGeneratorPrivate::insertFilterAttributes(
const QStringList &attributes)
716 m_query->exec(QLatin1String(
"SELECT Name FROM FilterAttributeTable"));
718 while (m_query->next())
719 atts.insert(m_query->value(0).toString());
721 for (
const QString &s : attributes) {
722 if (!atts.contains(s)) {
723 m_query->prepare(QLatin1String(
"INSERT INTO FilterAttributeTable VALUES(NULL, ?)"));
724 m_query->bindValue(0, s);
731bool HelpGeneratorPrivate::insertMetaData(
const QMap<QString, QVariant> &metaData)
736 for (
auto it = metaData.cbegin(), end = metaData.cend(); it != end; ++it) {
737 m_query->prepare(QLatin1String(
"INSERT INTO MetaDataTable VALUES(?, ?)"));
738 m_query->bindValue(0, it.key());
739 m_query->bindValue(1, it.value());
748
749
750
752 for (
const QHelpDataFilterSection &filterSection : helpData.filterSections()) {
753 for (
const QString &file : filterSection.files()) {
754 const QFileInfo fileInfo(helpData.rootPath() + QDir::separator() + file);
755 const QString &canonicalFileName = fileInfo.canonicalFilePath();
756 if (!fileInfo.exists())
757 emit warning(tr(
"File \"%1\" does not exist.").arg(file));
759 files.insert(canonicalFileName);
764
765
766
767
768
769 bool allLinksOk =
true;
770 for (
const QString &fileName : std::as_const(files)) {
771 if (!fileName.endsWith(QLatin1String(
"html"))
772 && !fileName.endsWith(QLatin1String(
"htm")))
774 QFile htmlFile(fileName);
775 if (!htmlFile.open(QIODevice::ReadOnly)) {
776 emit warning(tr(
"File \"%1\" cannot be opened.").arg(fileName));
779 const QRegularExpression linkPattern(QLatin1String(
"<(?:a href|img src)=\"?([^#\">]+)[#\">]"));
780 QByteArray data = htmlFile.readAll();
781 auto encoding = QStringDecoder::encodingForHtml(data);
783 encoding = QStringDecoder::Utf8;
784 const QString &content = QStringDecoder(*encoding)(data);
785 QStringList invalidLinks;
786 QRegularExpressionMatch match;
788 while ((match = linkPattern.match(content, pos)).hasMatch()) {
789 pos = match.capturedEnd();
790 const QString &linkedFileName = match.captured(1);
791 if (linkedFileName.contains(QLatin1String(
"://")))
793 const QString &curDir = QFileInfo(fileName).dir().path();
794 const QString &canonicalLinkedFileName =
795 QFileInfo(curDir + QDir::separator() + linkedFileName).canonicalFilePath();
796 if (!files.contains(canonicalLinkedFileName)
797 && !invalidLinks.contains(canonicalLinkedFileName)) {
798 emit warning(tr(
"File \"%1\" contains an invalid link to file \"%2\"").
799 arg(fileName).arg(linkedFileName));
801 invalidLinks.append(canonicalLinkedFileName);
807 m_error = tr(
"Invalid links in HTML files.");
815 m_private =
new HelpGeneratorPrivate(
this);
817 connect(m_private, &HelpGeneratorPrivate::statusChanged,
818 this, &HelpGenerator::printStatus);
820 connect(m_private, &HelpGeneratorPrivate::warning,
821 this, &HelpGenerator::printWarning);
825 const QString &outputFileName)
827 return m_private->generate(helpData, outputFileName);
832 return m_private->checkLinks(helpData);
837 return m_private->error();
842 puts(qPrintable(msg));
847 puts(qPrintable(tr(
"Warning: %1").arg(msg)));
852#include "helpgenerator.moc"
bool checkLinks(const QHelpProjectData &helpData)
The QHelpProjectData class stores all information found in a Qt help project file.