Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
helpgenerator.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3// Qt-Security score:insignificant reason:build-tool
4
7#include <qhelp_global.h>
8
9#include <QtCore/QtMath>
10#include <QtCore/QMap>
11#include <QtCore/QFile>
12#include <QtCore/QFileInfo>
13#include <QtCore/QDir>
14#include <QtCore/QDebug>
15#include <QtCore/QRegularExpression>
16#include <QtCore/QSet>
17#include <QtCore/QMap>
18#include <QtCore/QVariant>
19#include <QtCore/QDateTime>
20#include <QtCore/QStringConverter>
21#include <QtCore/QDataStream>
22#include <QtSql/QSqlQuery>
23
24#include <stdio.h>
25
26QT_BEGIN_NAMESPACE
27
28class HelpGeneratorPrivate : public QObject
29{
30 Q_OBJECT
31
32public:
33 HelpGeneratorPrivate(QObject *parent = nullptr) : QObject(parent) {}
34
35 bool generate(QHelpProjectData *helpData,
36 const QString &outputFileName);
37 bool checkLinks(const QHelpProjectData &helpData);
38 QString error() const;
39
40Q_SIGNALS:
41 void statusChanged(const QString &msg);
42 void progressChanged(double progress);
43 void warning(const QString &msg);
44
45private:
46 struct FileNameTableData
47 {
48 QString name;
49 int fileId;
50 QString title;
51 };
52
53 void writeTree(QDataStream &s, QHelpDataContentItem *item, int depth);
54 bool createTables();
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);
67 void cleanupDB();
68 void setupProgress(QHelpProjectData *helpData);
69 void addProgress(double step);
70
71 QString m_error;
72 QSqlQuery *m_query = nullptr;
73
74 int m_namespaceId = -1;
75 int m_virtualFolderId = -1;
76
77 QMap<QString, int> m_fileMap;
78 QMap<int, QSet<int> > m_fileFilterMap;
79
80 double m_progress;
81 double m_oldProgress;
82 double m_contentStep;
83 double m_fileStep;
84 double m_indexStep;
85};
86
87/*!
88 Takes the \a helpData and generates a new documentation
89 set from it. The Qt compressed help file is written to \a
90 outputFileName. Returns true on success, otherwise false.
91*/
92bool HelpGeneratorPrivate::generate(QHelpProjectData *helpData,
93 const QString &outputFileName)
94{
95 emit progressChanged(0);
96 m_error.clear();
97 if (!helpData || helpData->namespaceName().isEmpty()) {
98 m_error = tr("Invalid help data.");
99 return false;
100 }
101
102 QString outFileName = outputFileName;
103 if (outFileName.isEmpty()) {
104 m_error = tr("No output file name specified.");
105 return false;
106 }
107
108 QFileInfo fi(outFileName);
109 if (fi.exists()) {
110 if (!fi.dir().remove(fi.fileName())) {
111 m_error = tr("The file %1 cannot be overwritten.").arg(outFileName);
112 return false;
113 }
114 }
115
116 setupProgress(helpData);
117
118 emit statusChanged(tr("Building up file structure..."));
119 bool openingOk = true;
120 {
121 QSqlDatabase db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), QLatin1String("builder"));
122 db.setDatabaseName(outFileName);
123 openingOk = db.open();
124 if (openingOk)
125 m_query = new QSqlQuery(db);
126 }
127
128 if (!openingOk) {
129 m_error = tr("Cannot open data base file %1.").arg(outFileName);
130 cleanupDB();
131 return false;
132 }
133
134 m_query->exec(QLatin1String("PRAGMA synchronous=OFF"));
135 m_query->exec(QLatin1String("PRAGMA cache_size=3000"));
136
137 addProgress(1.0);
138 createTables();
139 insertFileNotFoundFile();
140 insertMetaData(helpData->metaData());
141
142 if (!registerVirtualFolder(helpData->virtualFolder(), helpData->namespaceName())) {
143 m_error = tr("Cannot register namespace \"%1\".").arg(helpData->namespaceName());
144 cleanupDB();
145 return false;
146 }
147 addProgress(1.0);
148
149 emit statusChanged(tr("Insert custom filters..."));
150 for (const QHelpDataCustomFilter &f : helpData->customFilters()) {
151 if (!registerCustomFilter(f.name, f.filterAttributes, true)) {
152 cleanupDB();
153 return false;
154 }
155 }
156 addProgress(1.0);
157
158 int i = 1;
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());
163 QByteArray ba;
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())) {
170 cleanupDB();
171 return false;
172 }
173 }
174
175 cleanupDB();
176 emit progressChanged(100);
177 emit statusChanged(tr("Documentation successfully generated."));
178 return true;
179}
180
181void HelpGeneratorPrivate::setupProgress(QHelpProjectData *helpData)
182{
183 m_progress = 0;
184 m_oldProgress = 0;
185
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();
191 }
192 // init 2%
193 // filters 1%
194 // contents 10%
195 // files 60%
196 // indices 27%
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);
200}
201
202void HelpGeneratorPrivate::addProgress(double step)
203{
204 m_progress += step;
205 if ((m_progress - m_oldProgress) >= 1.0 && m_progress <= 100.0) {
206 m_oldProgress = m_progress;
207 emit progressChanged(qCeil(m_progress));
208 }
209}
210
211void HelpGeneratorPrivate::cleanupDB()
212{
213 if (m_query) {
214 m_query->clear();
215 delete m_query;
216 m_query = nullptr;
217 }
218 QSqlDatabase::removeDatabase(QLatin1String("builder"));
219}
220
221void HelpGeneratorPrivate::writeTree(QDataStream &s, QHelpDataContentItem *item, int depth)
222{
223 s << depth;
224 s << item->reference();
225 s << item->title();
226 for (QHelpDataContentItem *i : item->children())
227 writeTree(s, i, depth + 1);
228}
229
230/*!
231 Returns the last error message.
232*/
233QString HelpGeneratorPrivate::error() const
234{
235 return m_error;
236}
237
238bool HelpGeneratorPrivate::createTables()
239{
240 if (!m_query)
241 return false;
242
243 m_query->exec(QLatin1String("SELECT COUNT(*) FROM sqlite_master WHERE TYPE=\'table\'"
244 "AND Name=\'NamespaceTable\'"));
245 m_query->next();
246 if (m_query->value(0).toInt() > 0) {
247 m_error = tr("Some tables already exist.");
248 return false;
249 }
250
251 const QStringList tables = QStringList()
252 << QLatin1String("CREATE TABLE NamespaceTable ("
253 "Id INTEGER PRIMARY KEY,"
254 "Name TEXT )")
255 << QLatin1String("CREATE TABLE FilterAttributeTable ("
256 "Id INTEGER PRIMARY KEY, "
257 "Name TEXT )")
258 << QLatin1String("CREATE TABLE FilterNameTable ("
259 "Id INTEGER PRIMARY KEY, "
260 "Name TEXT )")
261 << QLatin1String("CREATE TABLE FilterTable ("
262 "NameId INTEGER, "
263 "FilterAttributeId INTEGER )")
264 << QLatin1String("CREATE TABLE IndexTable ("
265 "Id INTEGER PRIMARY KEY, "
266 "Name TEXT, "
267 "Identifier TEXT, "
268 "NamespaceId INTEGER, "
269 "FileId INTEGER, "
270 "Anchor TEXT )")
271 << QLatin1String("CREATE TABLE IndexFilterTable ("
272 "FilterAttributeId INTEGER, "
273 "IndexId INTEGER )")
274 << QLatin1String("CREATE TABLE ContentsTable ("
275 "Id INTEGER PRIMARY KEY, "
276 "NamespaceId INTEGER, "
277 "Data BLOB )")
278 << QLatin1String("CREATE TABLE ContentsFilterTable ("
279 "FilterAttributeId INTEGER, "
280 "ContentsId INTEGER )")
281 << QLatin1String("CREATE TABLE FileAttributeSetTable ("
282 "Id INTEGER, "
283 "FilterAttributeId INTEGER )")
284 << QLatin1String("CREATE TABLE FileDataTable ("
285 "Id INTEGER PRIMARY KEY, "
286 "Data BLOB )")
287 << QLatin1String("CREATE TABLE FileFilterTable ("
288 "FilterAttributeId INTEGER, "
289 "FileId INTEGER )")
290 << QLatin1String("CREATE TABLE FileNameTable ("
291 "FolderId INTEGER, "
292 "Name TEXT, "
293 "FileId INTEGER, "
294 "Title TEXT )")
295 << QLatin1String("CREATE TABLE FolderTable("
296 "Id INTEGER PRIMARY KEY, "
297 "Name Text, "
298 "NamespaceID INTEGER )")
299 << QLatin1String("CREATE TABLE MetaDataTable("
300 "Name Text, "
301 "Value BLOB )");
302
303 for (const QString &q : tables) {
304 if (!m_query->exec(q)) {
305 m_error = tr("Cannot create tables.");
306 return false;
307 }
308 }
309
310 m_query->exec(QLatin1String("INSERT INTO MetaDataTable VALUES('qchVersion', '1.0')"));
311
312 return true;
313}
314
315bool HelpGeneratorPrivate::insertFileNotFoundFile()
316{
317 if (!m_query)
318 return false;
319
320 m_query->exec(QLatin1String("SELECT id FROM FileNameTable WHERE Name=\'\'"));
321 if (m_query->next() && m_query->isValid())
322 return true;
323
324 m_query->prepare(QLatin1String("INSERT INTO FileDataTable VALUES (Null, ?)"));
325 m_query->bindValue(0, QByteArray());
326 if (!m_query->exec())
327 return false;
328
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);
335 return true;
336 }
337 return false;
338}
339
340bool HelpGeneratorPrivate::registerVirtualFolder(const QString &folderName, const QString &ns)
341{
342 if (!m_query || folderName.isEmpty() || ns.isEmpty())
343 return false;
344
345 m_query->prepare(QLatin1String("SELECT Id FROM FolderTable WHERE Name=?"));
346 m_query->bindValue(0, folderName);
347 m_query->exec();
348 m_query->next();
349 if (m_query->isValid() && m_query->value(0).toInt() > 0)
350 return true;
351
352 m_namespaceId = -1;
353 m_query->prepare(QLatin1String("SELECT Id FROM NamespaceTable WHERE Name=?"));
354 m_query->bindValue(0, ns);
355 m_query->exec();
356 while (m_query->next()) {
357 m_namespaceId = m_query->value(0).toInt();
358 break;
359 }
360
361 if (m_namespaceId < 0) {
362 m_query->prepare(QLatin1String("INSERT INTO NamespaceTable VALUES(NULL, ?)"));
363 m_query->bindValue(0, ns);
364 if (m_query->exec())
365 m_namespaceId = m_query->lastInsertId().toInt();
366 }
367
368 if (m_namespaceId > 0) {
369 m_query->prepare(QLatin1String("SELECT Id FROM FolderTable WHERE Name=?"));
370 m_query->bindValue(0, folderName);
371 m_query->exec();
372 while (m_query->next())
373 m_virtualFolderId = m_query->value(0).toInt();
374
375 if (m_virtualFolderId > 0)
376 return true;
377
378 m_query->prepare(QLatin1String("INSERT INTO FolderTable (NamespaceId, Name) "
379 "VALUES (?, ?)"));
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;
385 }
386 }
387 m_error = tr("Cannot register virtual folder.");
388 return false;
389}
390
391bool HelpGeneratorPrivate::insertFiles(const QStringList &files, const QString &rootPath,
392 const QStringList &filterAttributes)
393{
394 if (!m_query)
395 return false;
396
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 "
401 "WHERE Name=?"));
402 m_query->bindValue(0, filterAtt);
403 m_query->exec();
404 if (m_query->next())
405 filterAtts.insert(m_query->value(0).toInt());
406 }
407
408 int filterSetId = -1;
409 m_query->exec(QLatin1String("SELECT MAX(Id) FROM FileAttributeSetTable"));
410 if (m_query->next())
411 filterSetId = m_query->value(0).toInt();
412 if (filterSetId < 0)
413 return false;
414 ++filterSetId;
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 "
419 "VALUES(?, ?)"));
420 m_query->bindValue(0, filterSetId);
421 m_query->bindValue(1, attId);
422 m_query->exec();
423 }
424
425 int tableFileId = 1;
426 m_query->exec(QLatin1String("SELECT MAX(Id) FROM FileDataTable"));
427 if (m_query->next())
428 tableFileId = m_query->value(0).toInt() + 1;
429
430 QString title;
431 QString charSet;
432 QList<QByteArray> fileDataList;
433 QMap<int, QSet<int> > tmpFileFilterMap;
434 QList<FileNameTableData> fileNameDataList;
435
436 int i = 0;
437 for (const QString &file : files) {
438 const QString fileName = QDir::cleanPath(file);
439
440 QFile fi(rootPath + QDir::separator() + fileName);
441 if (!fi.exists()) {
442 emit warning(tr("The file %1 does not exist, skipping it...")
443 .arg(QDir::cleanPath(rootPath + QDir::separator() + fileName)));
444 continue;
445 }
446
447 if (!fi.open(QIODevice::ReadOnly)) {
448 emit warning(tr("Cannot open file %1, skipping it...")
449 .arg(QDir::cleanPath(rootPath + QDir::separator() + fileName)));
450 continue;
451 }
452
453 QByteArray data = fi.readAll();
454 if (fileName.endsWith(QLatin1String(".html"))
455 || fileName.endsWith(QLatin1String(".htm"))) {
456 auto encoding = QStringDecoder::encodingForHtml(data);
457 if (!encoding)
458 encoding = QStringDecoder::Utf8;
459 title = QHelpGlobal::documentTitle(QStringDecoder(*encoding)(data));
460 } else {
461 title = fileName.mid(fileName.lastIndexOf(QLatin1Char('/')) + 1);
462 }
463
464 int fileId = -1;
465 const auto &it = m_fileMap.constFind(fileName);
466 if (it == m_fileMap.cend()) {
467 fileDataList.append(qCompress(data));
468
469 FileNameTableData fileNameData;
470 fileNameData.name = fileName;
471 fileNameData.fileId = tableFileId;
472 fileNameData.title = title;
473 fileNameDataList.append(fileNameData);
474
475 m_fileMap.insert(fileName, tableFileId);
476 m_fileFilterMap.insert(tableFileId, filterAtts);
477 tmpFileFilterMap.insert(tableFileId, filterAtts);
478
479 ++tableFileId;
480 } else {
481 fileId = it.value();
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);
489 }
490 }
491 }
492 }
493
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 "
501 "VALUES(?, ?)"));
502 m_query->bindValue(0, fv);
503 m_query->bindValue(1, it.key());
504 m_query->exec();
505 }
506 }
507
508 for (const QByteArray &fileData : std::as_const(fileDataList)) {
509 m_query->prepare(QLatin1String("INSERT INTO FileDataTable VALUES "
510 "(Null, ?)"));
511 m_query->bindValue(0, fileData);
512 m_query->exec();
513 if (++i % 20 == 0)
514 addProgress(m_fileStep * 20.0);
515 }
516
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);
524 m_query->exec();
525 }
526 m_query->exec(QLatin1String("COMMIT"));
527 }
528
529 m_query->exec(QLatin1String("SELECT MAX(Id) FROM FileDataTable"));
530 if (m_query->next()
531 && m_query->value(0).toInt() == tableFileId - 1) {
532 addProgress(m_fileStep*(i % 20));
533 return true;
534 }
535 return false;
536}
537
538bool HelpGeneratorPrivate::registerCustomFilter(const QString &filterName,
539 const QStringList &filterAttribs, bool forceUpdate)
540{
541 if (!m_query)
542 return false;
543
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());
551 }
552
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);
556 m_query->exec();
557 attributeMap.insert(id, m_query->lastInsertId().toInt());
558 }
559
560 int nameId = -1;
561 m_query->prepare(QLatin1String("SELECT Id FROM FilterNameTable WHERE Name=?"));
562 m_query->bindValue(0, filterName);
563 m_query->exec();
564 while (m_query->next()) {
565 nameId = m_query->value(0).toInt();
566 break;
567 }
568
569 if (nameId < 0) {
570 m_query->prepare(QLatin1String("INSERT INTO FilterNameTable VALUES(NULL, ?)"));
571 m_query->bindValue(0, filterName);
572 if (m_query->exec())
573 nameId = m_query->lastInsertId().toInt();
574 } else if (!forceUpdate) {
575 m_error = tr("The filter %1 is already registered.").arg(filterName);
576 return false;
577 }
578
579 if (nameId < 0) {
580 m_error = tr("Cannot register filter %1.").arg(filterName);
581 return false;
582 }
583
584 m_query->prepare(QLatin1String("DELETE FROM FilterTable WHERE NameId=?"));
585 m_query->bindValue(0, nameId);
586 m_query->exec();
587
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())
593 return false;
594 }
595 return true;
596}
597
598bool HelpGeneratorPrivate::insertKeywords(const QList<QHelpDataIndexItem> &keywords,
599 const QStringList &filterAttributes)
600{
601 if (!m_query)
602 return false;
603
604 emit statusChanged(tr("Insert indices..."));
605 int indexId = 1;
606 m_query->exec(QLatin1String("SELECT MAX(Id) FROM IndexTable"));
607 if (m_query->next())
608 indexId = m_query->value(0).toInt() + 1;
609
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);
614 m_query->exec();
615 if (m_query->next())
616 filterAtts.append(m_query->value(0).toInt());
617 }
618
619 QList<int> indexFilterTable;
620
621 int i = 0;
622 m_query->exec(QLatin1String("BEGIN"));
623 QSet<QString> indices;
624 for (const QHelpDataIndexItem &itm : keywords) {
625 // Identical ids make no sense and just confuse the Assistant user,
626 // so we ignore all repetitions.
627 if (indices.contains(itm.identifier))
628 continue;
629
630 // Still empty ids should be ignored, as otherwise we will include only
631 // the first keyword with an empty id.
632 if (!itm.identifier.isEmpty())
633 indices.insert(itm.identifier);
634
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);
638
639 const QString &fName = QDir::cleanPath(fileName);
640
641 const auto &it = m_fileMap.constFind(fName);
642 const int fileId = it == m_fileMap.cend() ? 1 : it.value();
643
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);
651 m_query->exec();
652
653 indexFilterTable.append(indexId++);
654 if (++i % 100 == 0)
655 addProgress(m_indexStep * 100.0);
656 }
657 m_query->exec(QLatin1String("COMMIT"));
658
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) "
663 "VALUES(?, ?)"));
664 m_query->bindValue(0, a);
665 m_query->bindValue(1, idx);
666 m_query->exec();
667 }
668 }
669 m_query->exec(QLatin1String("COMMIT"));
670
671 m_query->exec(QLatin1String("SELECT COUNT(Id) FROM IndexTable"));
672 if (m_query->next() && m_query->value(0).toInt() >= indices.size())
673 return true;
674 return false;
675}
676
677bool HelpGeneratorPrivate::insertContents(const QByteArray &ba,
678 const QStringList &filterAttributes)
679{
680 if (!m_query)
681 return false;
682
683 emit statusChanged(tr("Insert contents..."));
684 m_query->prepare(QLatin1String("INSERT INTO ContentsTable (NamespaceId, Data) "
685 "VALUES(?, ?)"));
686 m_query->bindValue(0, m_namespaceId);
687 m_query->bindValue(1, ba);
688 m_query->exec();
689 int contentId = m_query->lastInsertId().toInt();
690 if (contentId < 1) {
691 m_error = tr("Cannot insert contents.");
692 return false;
693 }
694
695 // associate the filter attributes
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);
701 m_query->exec();
702 if (!m_query->isActive()) {
703 m_error = tr("Cannot register contents.");
704 return false;
705 }
706 }
707 addProgress(m_contentStep);
708 return true;
709}
710
711bool HelpGeneratorPrivate::insertFilterAttributes(const QStringList &attributes)
712{
713 if (!m_query)
714 return false;
715
716 m_query->exec(QLatin1String("SELECT Name FROM FilterAttributeTable"));
717 QSet<QString> atts;
718 while (m_query->next())
719 atts.insert(m_query->value(0).toString());
720
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);
725 m_query->exec();
726 }
727 }
728 return true;
729}
730
731bool HelpGeneratorPrivate::insertMetaData(const QMap<QString, QVariant> &metaData)
732{
733 if (!m_query)
734 return false;
735
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());
740 m_query->exec();
741 }
742 return true;
743}
744
745bool HelpGeneratorPrivate::checkLinks(const QHelpProjectData &helpData)
746{
747 /*
748 * Step 1: Gather the canoncal file paths of all files in the project.
749 * We use a set, because there will be a lot of look-ups.
750 */
751 QSet<QString> files;
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));
758 else
759 files.insert(canonicalFileName);
760 }
761 }
762
763 /*
764 * Step 2: Check the hypertext and image references of all HTML files.
765 * Note that we don't parse the files, but simply grep for the
766 * respective HTML elements. Therefore. contents that are e.g.
767 * commented out can cause false warning.
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")))
773 continue;
774 QFile htmlFile(fileName);
775 if (!htmlFile.open(QIODevice::ReadOnly)) {
776 emit warning(tr("File \"%1\" cannot be opened.").arg(fileName));
777 continue;
778 }
779 const QRegularExpression linkPattern(QLatin1String("<(?:a href|img src)=\"?([^#\">]+)[#\">]"));
780 QByteArray data = htmlFile.readAll();
781 auto encoding = QStringDecoder::encodingForHtml(data);
782 if (!encoding)
783 encoding = QStringDecoder::Utf8;
784 const QString &content = QStringDecoder(*encoding)(data);
785 QStringList invalidLinks;
786 QRegularExpressionMatch match;
787 int pos = 0;
788 while ((match = linkPattern.match(content, pos)).hasMatch()) {
789 pos = match.capturedEnd();
790 const QString &linkedFileName = match.captured(1);
791 if (linkedFileName.contains(QLatin1String("://")))
792 continue;
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));
800 allLinksOk = false;
801 invalidLinks.append(canonicalLinkedFileName);
802 }
803 }
804 }
805
806 if (!allLinksOk)
807 m_error = tr("Invalid links in HTML files.");
808 return allLinksOk;
809}
810
811//////////////////////////////
812
813HelpGenerator::HelpGenerator(bool silent)
814{
815 m_private = new HelpGeneratorPrivate(this);
816 if (!silent) {
817 connect(m_private, &HelpGeneratorPrivate::statusChanged,
818 this, &HelpGenerator::printStatus);
819 }
820 connect(m_private, &HelpGeneratorPrivate::warning,
821 this, &HelpGenerator::printWarning);
822}
823
824bool HelpGenerator::generate(QHelpProjectData *helpData,
825 const QString &outputFileName)
826{
827 return m_private->generate(helpData, outputFileName);
828}
829
831{
832 return m_private->checkLinks(helpData);
833}
834
835QString HelpGenerator::error() const
836{
837 return m_private->error();
838}
839
840void HelpGenerator::printStatus(const QString &msg)
841{
842 puts(qPrintable(msg));
843}
844
845void HelpGenerator::printWarning(const QString &msg)
846{
847 puts(qPrintable(tr("Warning: %1").arg(msg)));
848}
849
850QT_END_NAMESPACE
851
852#include "helpgenerator.moc"
QString error() const
bool checkLinks(const QHelpProjectData &helpData)
The QHelpProjectData class stores all information found in a Qt help project file.