1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt Assistant of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "helpgenerator.h"
41#include "qhelpprojectdata_p.h"
42#include <qhelp_global.h>
43
44#include <QtCore/QtMath>
45#include <QtCore/QFile>
46#include <QtCore/QFileInfo>
47#include <QtCore/QDir>
48#include <QtCore/QDebug>
49#include <QtCore/QRegExp>
50#include <QtCore/QSet>
51#include <QtCore/QVariant>
52#include <QtCore/QDateTime>
53#include <QtCore/QTextCodec>
54#include <QtCore/QDataStream>
55#include <QtSql/QSqlQuery>
56
57#include <stdio.h>
58
59QT_BEGIN_NAMESPACE
60
61class HelpGeneratorPrivate : public QObject
62{
63 Q_OBJECT
64
65public:
66 HelpGeneratorPrivate(QObject *parent = nullptr) : QObject(parent) {}
67
68 bool generate(QHelpProjectData *helpData,
69 const QString &outputFileName);
70 bool checkLinks(const QHelpProjectData &helpData);
71 QString error() const;
72
73Q_SIGNALS:
74 void statusChanged(const QString &msg);
75 void progressChanged(double progress);
76 void warning(const QString &msg);
77
78private:
79 struct FileNameTableData
80 {
81 QString name;
82 int fileId;
83 QString title;
84 };
85
86 void writeTree(QDataStream &s, QHelpDataContentItem *item, int depth);
87 bool createTables();
88 bool insertFileNotFoundFile();
89 bool registerCustomFilter(const QString &filterName,
90 const QStringList &filterAttribs, bool forceUpdate = false);
91 bool registerVirtualFolder(const QString &folderName, const QString &ns);
92 bool insertFilterAttributes(const QStringList &attributes);
93 bool insertKeywords(const QList<QHelpDataIndexItem> &keywords,
94 const QStringList &filterAttributes);
95 bool insertFiles(const QStringList &files, const QString &rootPath,
96 const QStringList &filterAttributes);
97 bool insertContents(const QByteArray &ba,
98 const QStringList &filterAttributes);
99 bool insertMetaData(const QMap<QString, QVariant> &metaData);
100 void cleanupDB();
101 void setupProgress(QHelpProjectData *helpData);
102 void addProgress(double step);
103
104 QString m_error;
105 QSqlQuery *m_query = nullptr;
106
107 int m_namespaceId = -1;
108 int m_virtualFolderId = -1;
109
110 QMap<QString, int> m_fileMap;
111 QMap<int, QSet<int> > m_fileFilterMap;
112
113 double m_progress;
114 double m_oldProgress;
115 double m_contentStep;
116 double m_fileStep;
117 double m_indexStep;
118};
119
120/*!
121 Takes the \a helpData and generates a new documentation
122 set from it. The Qt compressed help file is written to \a
123 outputFileName. Returns true on success, otherwise false.
124*/
125bool HelpGeneratorPrivate::generate(QHelpProjectData *helpData,
126 const QString &outputFileName)
127{
128 emit progressChanged(progress: 0);
129 m_error.clear();
130 if (!helpData || helpData->namespaceName().isEmpty()) {
131 m_error = tr(s: "Invalid help data.");
132 return false;
133 }
134
135 QString outFileName = outputFileName;
136 if (outFileName.isEmpty()) {
137 m_error = tr(s: "No output file name specified.");
138 return false;
139 }
140
141 QFileInfo fi(outFileName);
142 if (fi.exists()) {
143 if (!fi.dir().remove(fileName: fi.fileName())) {
144 m_error = tr(s: "The file %1 cannot be overwritten.").arg(a: outFileName);
145 return false;
146 }
147 }
148
149 setupProgress(helpData);
150
151 emit statusChanged(msg: tr(s: "Building up file structure..."));
152 bool openingOk = true;
153 {
154 QSqlDatabase db = QSqlDatabase::addDatabase(type: QLatin1String("QSQLITE"), connectionName: QLatin1String("builder"));
155 db.setDatabaseName(outFileName);
156 openingOk = db.open();
157 if (openingOk)
158 m_query = new QSqlQuery(db);
159 }
160
161 if (!openingOk) {
162 m_error = tr(s: "Cannot open data base file %1.").arg(a: outFileName);
163 cleanupDB();
164 return false;
165 }
166
167 m_query->exec(query: QLatin1String("PRAGMA synchronous=OFF"));
168 m_query->exec(query: QLatin1String("PRAGMA cache_size=3000"));
169
170 addProgress(step: 1.0);
171 createTables();
172 insertFileNotFoundFile();
173 insertMetaData(metaData: helpData->metaData());
174
175 if (!registerVirtualFolder(folderName: helpData->virtualFolder(), ns: helpData->namespaceName())) {
176 m_error = tr(s: "Cannot register namespace \"%1\".").arg(a: helpData->namespaceName());
177 cleanupDB();
178 return false;
179 }
180 addProgress(step: 1.0);
181
182 emit statusChanged(msg: tr(s: "Insert custom filters..."));
183 for (const QHelpDataCustomFilter &f : helpData->customFilters()) {
184 if (!registerCustomFilter(filterName: f.name, filterAttribs: f.filterAttributes, forceUpdate: true)) {
185 cleanupDB();
186 return false;
187 }
188 }
189 addProgress(step: 1.0);
190
191 int i = 1;
192 for (const QHelpDataFilterSection &fs : helpData->filterSections()) {
193 emit statusChanged(msg: tr(s: "Insert help data for filter section (%1 of %2)...")
194 .arg(a: i++).arg(a: helpData->filterSections().count()));
195 insertFilterAttributes(attributes: fs.filterAttributes());
196 QByteArray ba;
197 QDataStream s(&ba, QIODevice::WriteOnly);
198 for (QHelpDataContentItem *itm : fs.contents())
199 writeTree(s, item: itm, depth: 0);
200 if (!insertFiles(files: fs.files(), rootPath: helpData->rootPath(), filterAttributes: fs.filterAttributes())
201 || !insertContents(ba, filterAttributes: fs.filterAttributes())
202 || !insertKeywords(keywords: fs.indices(), filterAttributes: fs.filterAttributes())) {
203 cleanupDB();
204 return false;
205 }
206 }
207
208 cleanupDB();
209 emit progressChanged(progress: 100);
210 emit statusChanged(msg: tr(s: "Documentation successfully generated."));
211 return true;
212}
213
214void HelpGeneratorPrivate::setupProgress(QHelpProjectData *helpData)
215{
216 m_progress = 0;
217 m_oldProgress = 0;
218
219 int numberOfFiles = 0;
220 int numberOfIndices = 0;
221 for (const QHelpDataFilterSection &fs : helpData->filterSections()) {
222 numberOfFiles += fs.files().count();
223 numberOfIndices += fs.indices().count();
224 }
225 // init 2%
226 // filters 1%
227 // contents 10%
228 // files 60%
229 // indices 27%
230 m_contentStep = 10.0 / qMax(a: helpData->customFilters().count(), b: 1);
231 m_fileStep = 60.0 / qMax(a: numberOfFiles, b: 1);
232 m_indexStep = 27.0 / qMax(a: numberOfIndices, b: 1);
233}
234
235void HelpGeneratorPrivate::addProgress(double step)
236{
237 m_progress += step;
238 if ((m_progress - m_oldProgress) >= 1.0 && m_progress <= 100.0) {
239 m_oldProgress = m_progress;
240 emit progressChanged(progress: qCeil(v: m_progress));
241 }
242}
243
244void HelpGeneratorPrivate::cleanupDB()
245{
246 if (m_query) {
247 m_query->clear();
248 delete m_query;
249 m_query = nullptr;
250 }
251 QSqlDatabase::removeDatabase(connectionName: QLatin1String("builder"));
252}
253
254void HelpGeneratorPrivate::writeTree(QDataStream &s, QHelpDataContentItem *item, int depth)
255{
256 s << depth;
257 s << item->reference();
258 s << item->title();
259 for (QHelpDataContentItem *i : item->children())
260 writeTree(s, item: i, depth: depth + 1);
261}
262
263/*!
264 Returns the last error message.
265*/
266QString HelpGeneratorPrivate::error() const
267{
268 return m_error;
269}
270
271bool HelpGeneratorPrivate::createTables()
272{
273 if (!m_query)
274 return false;
275
276 m_query->exec(query: QLatin1String("SELECT COUNT(*) FROM sqlite_master WHERE TYPE=\'table\'"
277 "AND Name=\'NamespaceTable\'"));
278 m_query->next();
279 if (m_query->value(i: 0).toInt() > 0) {
280 m_error = tr(s: "Some tables already exist.");
281 return false;
282 }
283
284 const QStringList tables = QStringList()
285 << QLatin1String("CREATE TABLE NamespaceTable ("
286 "Id INTEGER PRIMARY KEY,"
287 "Name TEXT )")
288 << QLatin1String("CREATE TABLE FilterAttributeTable ("
289 "Id INTEGER PRIMARY KEY, "
290 "Name TEXT )")
291 << QLatin1String("CREATE TABLE FilterNameTable ("
292 "Id INTEGER PRIMARY KEY, "
293 "Name TEXT )")
294 << QLatin1String("CREATE TABLE FilterTable ("
295 "NameId INTEGER, "
296 "FilterAttributeId INTEGER )")
297 << QLatin1String("CREATE TABLE IndexTable ("
298 "Id INTEGER PRIMARY KEY, "
299 "Name TEXT, "
300 "Identifier TEXT, "
301 "NamespaceId INTEGER, "
302 "FileId INTEGER, "
303 "Anchor TEXT )")
304 << QLatin1String("CREATE TABLE IndexFilterTable ("
305 "FilterAttributeId INTEGER, "
306 "IndexId INTEGER )")
307 << QLatin1String("CREATE TABLE ContentsTable ("
308 "Id INTEGER PRIMARY KEY, "
309 "NamespaceId INTEGER, "
310 "Data BLOB )")
311 << QLatin1String("CREATE TABLE ContentsFilterTable ("
312 "FilterAttributeId INTEGER, "
313 "ContentsId INTEGER )")
314 << QLatin1String("CREATE TABLE FileAttributeSetTable ("
315 "Id INTEGER, "
316 "FilterAttributeId INTEGER )")
317 << QLatin1String("CREATE TABLE FileDataTable ("
318 "Id INTEGER PRIMARY KEY, "
319 "Data BLOB )")
320 << QLatin1String("CREATE TABLE FileFilterTable ("
321 "FilterAttributeId INTEGER, "
322 "FileId INTEGER )")
323 << QLatin1String("CREATE TABLE FileNameTable ("
324 "FolderId INTEGER, "
325 "Name TEXT, "
326 "FileId INTEGER, "
327 "Title TEXT )")
328 << QLatin1String("CREATE TABLE FolderTable("
329 "Id INTEGER PRIMARY KEY, "
330 "Name Text, "
331 "NamespaceID INTEGER )")
332 << QLatin1String("CREATE TABLE MetaDataTable("
333 "Name Text, "
334 "Value BLOB )");
335
336 for (const QString &q : tables) {
337 if (!m_query->exec(query: q)) {
338 m_error = tr(s: "Cannot create tables.");
339 return false;
340 }
341 }
342
343 m_query->exec(query: QLatin1String("INSERT INTO MetaDataTable VALUES('qchVersion', '1.0')"));
344
345 return true;
346}
347
348bool HelpGeneratorPrivate::insertFileNotFoundFile()
349{
350 if (!m_query)
351 return false;
352
353 m_query->exec(query: QLatin1String("SELECT id FROM FileNameTable WHERE Name=\'\'"));
354 if (m_query->next() && m_query->isValid())
355 return true;
356
357 m_query->prepare(query: QLatin1String("INSERT INTO FileDataTable VALUES (Null, ?)"));
358 m_query->bindValue(pos: 0, val: QByteArray());
359 if (!m_query->exec())
360 return false;
361
362 const int fileId = m_query->lastInsertId().toInt();
363 m_query->prepare(query: QLatin1String("INSERT INTO FileNameTable (FolderId, Name, FileId, Title) "
364 " VALUES (0, '', ?, '')"));
365 m_query->bindValue(pos: 0, val: fileId);
366 if (fileId > -1 && m_query->exec()) {
367 m_fileMap.insert(akey: QString(), avalue: fileId);
368 return true;
369 }
370 return false;
371}
372
373bool HelpGeneratorPrivate::registerVirtualFolder(const QString &folderName, const QString &ns)
374{
375 if (!m_query || folderName.isEmpty() || ns.isEmpty())
376 return false;
377
378 m_query->prepare(query: QLatin1String("SELECT Id FROM FolderTable WHERE Name=?"));
379 m_query->bindValue(pos: 0, val: folderName);
380 m_query->exec();
381 m_query->next();
382 if (m_query->isValid() && m_query->value(i: 0).toInt() > 0)
383 return true;
384
385 m_namespaceId = -1;
386 m_query->prepare(query: QLatin1String("SELECT Id FROM NamespaceTable WHERE Name=?"));
387 m_query->bindValue(pos: 0, val: ns);
388 m_query->exec();
389 while (m_query->next()) {
390 m_namespaceId = m_query->value(i: 0).toInt();
391 break;
392 }
393
394 if (m_namespaceId < 0) {
395 m_query->prepare(query: QLatin1String("INSERT INTO NamespaceTable VALUES(NULL, ?)"));
396 m_query->bindValue(pos: 0, val: ns);
397 if (m_query->exec())
398 m_namespaceId = m_query->lastInsertId().toInt();
399 }
400
401 if (m_namespaceId > 0) {
402 m_query->prepare(query: QLatin1String("SELECT Id FROM FolderTable WHERE Name=?"));
403 m_query->bindValue(pos: 0, val: folderName);
404 m_query->exec();
405 while (m_query->next())
406 m_virtualFolderId = m_query->value(i: 0).toInt();
407
408 if (m_virtualFolderId > 0)
409 return true;
410
411 m_query->prepare(query: QLatin1String("INSERT INTO FolderTable (NamespaceId, Name) "
412 "VALUES (?, ?)"));
413 m_query->bindValue(pos: 0, val: m_namespaceId);
414 m_query->bindValue(pos: 1, val: folderName);
415 if (m_query->exec()) {
416 m_virtualFolderId = m_query->lastInsertId().toInt();
417 return m_virtualFolderId > 0;
418 }
419 }
420 m_error = tr(s: "Cannot register virtual folder.");
421 return false;
422}
423
424bool HelpGeneratorPrivate::insertFiles(const QStringList &files, const QString &rootPath,
425 const QStringList &filterAttributes)
426{
427 if (!m_query)
428 return false;
429
430 emit statusChanged(msg: tr(s: "Insert files..."));
431 QSet<int> filterAtts;
432 for (const QString &filterAtt : filterAttributes) {
433 m_query->prepare(query: QLatin1String("SELECT Id FROM FilterAttributeTable "
434 "WHERE Name=?"));
435 m_query->bindValue(pos: 0, val: filterAtt);
436 m_query->exec();
437 if (m_query->next())
438 filterAtts.insert(value: m_query->value(i: 0).toInt());
439 }
440
441 int filterSetId = -1;
442 m_query->exec(query: QLatin1String("SELECT MAX(Id) FROM FileAttributeSetTable"));
443 if (m_query->next())
444 filterSetId = m_query->value(i: 0).toInt();
445 if (filterSetId < 0)
446 return false;
447 ++filterSetId;
448 for (int attId : qAsConst(t&: filterAtts)) {
449 m_query->prepare(query: QLatin1String("INSERT INTO FileAttributeSetTable "
450 "VALUES(?, ?)"));
451 m_query->bindValue(pos: 0, val: filterSetId);
452 m_query->bindValue(pos: 1, val: attId);
453 m_query->exec();
454 }
455
456 int tableFileId = 1;
457 m_query->exec(query: QLatin1String("SELECT MAX(Id) FROM FileDataTable"));
458 if (m_query->next())
459 tableFileId = m_query->value(i: 0).toInt() + 1;
460
461 QString title;
462 QString charSet;
463 QList<QByteArray> fileDataList;
464 QMap<int, QSet<int> > tmpFileFilterMap;
465 QList<FileNameTableData> fileNameDataList;
466
467 int i = 0;
468 for (const QString &file : files) {
469 const QString fileName = QDir::cleanPath(path: file);
470
471 QFile fi(rootPath + QDir::separator() + fileName);
472 if (!fi.exists()) {
473 emit warning(msg: tr(s: "The file %1 does not exist, skipping it...")
474 .arg(a: QDir::cleanPath(path: rootPath + QDir::separator() + fileName)));
475 continue;
476 }
477
478 if (!fi.open(flags: QIODevice::ReadOnly)) {
479 emit warning(msg: tr(s: "Cannot open file %1, skipping it...")
480 .arg(a: QDir::cleanPath(path: rootPath + QDir::separator() + fileName)));
481 continue;
482 }
483
484 QByteArray data = fi.readAll();
485 if (fileName.endsWith(s: QLatin1String(".html"))
486 || fileName.endsWith(s: QLatin1String(".htm"))) {
487 charSet = QHelpGlobal::codecFromData(data);
488 QTextStream stream(&data);
489 stream.setCodec(QTextCodec::codecForName(name: charSet.toLatin1().constData()));
490 title = QHelpGlobal::documentTitle(content: stream.readAll());
491 } else {
492 title = fileName.mid(position: fileName.lastIndexOf(c: QLatin1Char('/')) + 1);
493 }
494
495 int fileId = -1;
496 const auto &it = m_fileMap.constFind(akey: fileName);
497 if (it == m_fileMap.cend()) {
498 fileDataList.append(t: qCompress(data));
499
500 FileNameTableData fileNameData;
501 fileNameData.name = fileName;
502 fileNameData.fileId = tableFileId;
503 fileNameData.title = title;
504 fileNameDataList.append(t: fileNameData);
505
506 m_fileMap.insert(akey: fileName, avalue: tableFileId);
507 m_fileFilterMap.insert(akey: tableFileId, avalue: filterAtts);
508 tmpFileFilterMap.insert(akey: tableFileId, avalue: filterAtts);
509
510 ++tableFileId;
511 } else {
512 fileId = it.value();
513 QSet<int> &fileFilterSet = m_fileFilterMap[fileId];
514 QSet<int> &tmpFileFilterSet = tmpFileFilterMap[fileId];
515 for (int filter : qAsConst(t&: filterAtts)) {
516 if (!fileFilterSet.contains(value: filter)
517 && !tmpFileFilterSet.contains(value: filter)) {
518 fileFilterSet.insert(value: filter);
519 tmpFileFilterSet.insert(value: filter);
520 }
521 }
522 }
523 }
524
525 if (!tmpFileFilterMap.isEmpty()) {
526 m_query->exec(query: QLatin1String("BEGIN"));
527 for (auto it = tmpFileFilterMap.cbegin(), end = tmpFileFilterMap.cend(); it != end; ++it) {
528 QList<int> filterValues = it.value().values();
529 std::sort(first: filterValues.begin(), last: filterValues.end());
530 for (int fv : qAsConst(t&: filterValues)) {
531 m_query->prepare(query: QLatin1String("INSERT INTO FileFilterTable "
532 "VALUES(?, ?)"));
533 m_query->bindValue(pos: 0, val: fv);
534 m_query->bindValue(pos: 1, val: it.key());
535 m_query->exec();
536 }
537 }
538
539 for (const QByteArray &fileData : qAsConst(t&: fileDataList)) {
540 m_query->prepare(query: QLatin1String("INSERT INTO FileDataTable VALUES "
541 "(Null, ?)"));
542 m_query->bindValue(pos: 0, val: fileData);
543 m_query->exec();
544 if (++i % 20 == 0)
545 addProgress(step: m_fileStep * 20.0);
546 }
547
548 for (const FileNameTableData &fnd : qAsConst(t&: fileNameDataList)) {
549 m_query->prepare(query: QLatin1String("INSERT INTO FileNameTable "
550 "(FolderId, Name, FileId, Title) VALUES (?, ?, ?, ?)"));
551 m_query->bindValue(pos: 0, val: 1);
552 m_query->bindValue(pos: 1, val: fnd.name);
553 m_query->bindValue(pos: 2, val: fnd.fileId);
554 m_query->bindValue(pos: 3, val: fnd.title);
555 m_query->exec();
556 }
557 m_query->exec(query: QLatin1String("COMMIT"));
558 }
559
560 m_query->exec(query: QLatin1String("SELECT MAX(Id) FROM FileDataTable"));
561 if (m_query->next()
562 && m_query->value(i: 0).toInt() == tableFileId - 1) {
563 addProgress(step: m_fileStep*(i % 20));
564 return true;
565 }
566 return false;
567}
568
569bool HelpGeneratorPrivate::registerCustomFilter(const QString &filterName,
570 const QStringList &filterAttribs, bool forceUpdate)
571{
572 if (!m_query)
573 return false;
574
575 m_query->exec(query: QLatin1String("SELECT Id, Name FROM FilterAttributeTable"));
576 QStringList idsToInsert = filterAttribs;
577 QMap<QString, int> attributeMap;
578 while (m_query->next()) {
579 attributeMap.insert(akey: m_query->value(i: 1).toString(),
580 avalue: m_query->value(i: 0).toInt());
581 idsToInsert.removeAll(t: m_query->value(i: 1).toString());
582 }
583
584 for (const QString &id : qAsConst(t&: idsToInsert)) {
585 m_query->prepare(query: QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)"));
586 m_query->bindValue(pos: 0, val: id);
587 m_query->exec();
588 attributeMap.insert(akey: id, avalue: m_query->lastInsertId().toInt());
589 }
590
591 int nameId = -1;
592 m_query->prepare(query: QLatin1String("SELECT Id FROM FilterNameTable WHERE Name=?"));
593 m_query->bindValue(pos: 0, val: filterName);
594 m_query->exec();
595 while (m_query->next()) {
596 nameId = m_query->value(i: 0).toInt();
597 break;
598 }
599
600 if (nameId < 0) {
601 m_query->prepare(query: QLatin1String("INSERT INTO FilterNameTable VALUES(NULL, ?)"));
602 m_query->bindValue(pos: 0, val: filterName);
603 if (m_query->exec())
604 nameId = m_query->lastInsertId().toInt();
605 } else if (!forceUpdate) {
606 m_error = tr(s: "The filter %1 is already registered.").arg(a: filterName);
607 return false;
608 }
609
610 if (nameId < 0) {
611 m_error = tr(s: "Cannot register filter %1.").arg(a: filterName);
612 return false;
613 }
614
615 m_query->prepare(query: QLatin1String("DELETE FROM FilterTable WHERE NameId=?"));
616 m_query->bindValue(pos: 0, val: nameId);
617 m_query->exec();
618
619 for (const QString &att : filterAttribs) {
620 m_query->prepare(query: QLatin1String("INSERT INTO FilterTable VALUES(?, ?)"));
621 m_query->bindValue(pos: 0, val: nameId);
622 m_query->bindValue(pos: 1, val: attributeMap[att]);
623 if (!m_query->exec())
624 return false;
625 }
626 return true;
627}
628
629bool HelpGeneratorPrivate::insertKeywords(const QList<QHelpDataIndexItem> &keywords,
630 const QStringList &filterAttributes)
631{
632 if (!m_query)
633 return false;
634
635 emit statusChanged(msg: tr(s: "Insert indices..."));
636 int indexId = 1;
637 m_query->exec(query: QLatin1String("SELECT MAX(Id) FROM IndexTable"));
638 if (m_query->next())
639 indexId = m_query->value(i: 0).toInt() + 1;
640
641 QList<int> filterAtts;
642 for (const QString &filterAtt : filterAttributes) {
643 m_query->prepare(query: QLatin1String("SELECT Id FROM FilterAttributeTable WHERE Name=?"));
644 m_query->bindValue(pos: 0, val: filterAtt);
645 m_query->exec();
646 if (m_query->next())
647 filterAtts.append(t: m_query->value(i: 0).toInt());
648 }
649
650 QList<int> indexFilterTable;
651
652 int i = 0;
653 m_query->exec(query: QLatin1String("BEGIN"));
654 QSet<QString> indices;
655 for (const QHelpDataIndexItem &itm : keywords) {
656 // Identical ids make no sense and just confuse the Assistant user,
657 // so we ignore all repetitions.
658 if (indices.contains(value: itm.identifier))
659 continue;
660
661 // Still empty ids should be ignored, as otherwise we will include only
662 // the first keyword with an empty id.
663 if (!itm.identifier.isEmpty())
664 indices.insert(value: itm.identifier);
665
666 const int pos = itm.reference.indexOf(c: QLatin1Char('#'));
667 const QString &fileName = itm.reference.left(n: pos);
668 const QString anchor = pos < 0 ? QString() : itm.reference.mid(position: pos + 1);
669
670 const QString &fName = QDir::cleanPath(path: fileName);
671
672 const auto &it = m_fileMap.constFind(akey: fName);
673 const int fileId = it == m_fileMap.cend() ? 1 : it.value();
674
675 m_query->prepare(query: QLatin1String("INSERT INTO IndexTable (Name, Identifier, NamespaceId, FileId, Anchor) "
676 "VALUES(?, ?, ?, ?, ?)"));
677 m_query->bindValue(pos: 0, val: itm.name);
678 m_query->bindValue(pos: 1, val: itm.identifier);
679 m_query->bindValue(pos: 2, val: m_namespaceId);
680 m_query->bindValue(pos: 3, val: fileId);
681 m_query->bindValue(pos: 4, val: anchor);
682 m_query->exec();
683
684 indexFilterTable.append(t: indexId++);
685 if (++i % 100 == 0)
686 addProgress(step: m_indexStep * 100.0);
687 }
688 m_query->exec(query: QLatin1String("COMMIT"));
689
690 m_query->exec(query: QLatin1String("BEGIN"));
691 for (int idx : qAsConst(t&: indexFilterTable)) {
692 for (int a : qAsConst(t&: filterAtts)) {
693 m_query->prepare(query: QLatin1String("INSERT INTO IndexFilterTable (FilterAttributeId, IndexId) "
694 "VALUES(?, ?)"));
695 m_query->bindValue(pos: 0, val: a);
696 m_query->bindValue(pos: 1, val: idx);
697 m_query->exec();
698 }
699 }
700 m_query->exec(query: QLatin1String("COMMIT"));
701
702 m_query->exec(query: QLatin1String("SELECT COUNT(Id) FROM IndexTable"));
703 if (m_query->next() && m_query->value(i: 0).toInt() >= indices.count())
704 return true;
705 return false;
706}
707
708bool HelpGeneratorPrivate::insertContents(const QByteArray &ba,
709 const QStringList &filterAttributes)
710{
711 if (!m_query)
712 return false;
713
714 emit statusChanged(msg: tr(s: "Insert contents..."));
715 m_query->prepare(query: QLatin1String("INSERT INTO ContentsTable (NamespaceId, Data) "
716 "VALUES(?, ?)"));
717 m_query->bindValue(pos: 0, val: m_namespaceId);
718 m_query->bindValue(pos: 1, val: ba);
719 m_query->exec();
720 int contentId = m_query->lastInsertId().toInt();
721 if (contentId < 1) {
722 m_error = tr(s: "Cannot insert contents.");
723 return false;
724 }
725
726 // associate the filter attributes
727 for (const QString &filterAtt : filterAttributes) {
728 m_query->prepare(query: QLatin1String("INSERT INTO ContentsFilterTable (FilterAttributeId, ContentsId) "
729 "SELECT Id, ? FROM FilterAttributeTable WHERE Name=?"));
730 m_query->bindValue(pos: 0, val: contentId);
731 m_query->bindValue(pos: 1, val: filterAtt);
732 m_query->exec();
733 if (!m_query->isActive()) {
734 m_error = tr(s: "Cannot register contents.");
735 return false;
736 }
737 }
738 addProgress(step: m_contentStep);
739 return true;
740}
741
742bool HelpGeneratorPrivate::insertFilterAttributes(const QStringList &attributes)
743{
744 if (!m_query)
745 return false;
746
747 m_query->exec(query: QLatin1String("SELECT Name FROM FilterAttributeTable"));
748 QSet<QString> atts;
749 while (m_query->next())
750 atts.insert(value: m_query->value(i: 0).toString());
751
752 for (const QString &s : attributes) {
753 if (!atts.contains(value: s)) {
754 m_query->prepare(query: QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)"));
755 m_query->bindValue(pos: 0, val: s);
756 m_query->exec();
757 }
758 }
759 return true;
760}
761
762bool HelpGeneratorPrivate::insertMetaData(const QMap<QString, QVariant> &metaData)
763{
764 if (!m_query)
765 return false;
766
767 for (auto it = metaData.cbegin(), end = metaData.cend(); it != end; ++it) {
768 m_query->prepare(query: QLatin1String("INSERT INTO MetaDataTable VALUES(?, ?)"));
769 m_query->bindValue(pos: 0, val: it.key());
770 m_query->bindValue(pos: 1, val: it.value());
771 m_query->exec();
772 }
773 return true;
774}
775
776bool HelpGeneratorPrivate::checkLinks(const QHelpProjectData &helpData)
777{
778 /*
779 * Step 1: Gather the canoncal file paths of all files in the project.
780 * We use a set, because there will be a lot of look-ups.
781 */
782 QSet<QString> files;
783 for (const QHelpDataFilterSection &filterSection : helpData.filterSections()) {
784 for (const QString &file : filterSection.files()) {
785 const QFileInfo fileInfo(helpData.rootPath() + QDir::separator() + file);
786 const QString &canonicalFileName = fileInfo.canonicalFilePath();
787 if (!fileInfo.exists())
788 emit warning(msg: tr(s: "File \"%1\" does not exist.").arg(a: file));
789 else
790 files.insert(value: canonicalFileName);
791 }
792 }
793
794 /*
795 * Step 2: Check the hypertext and image references of all HTML files.
796 * Note that we don't parse the files, but simply grep for the
797 * respective HTML elements. Therefore. contents that are e.g.
798 * commented out can cause false warning.
799 */
800 bool allLinksOk = true;
801 for (const QString &fileName : qAsConst(t&: files)) {
802 if (!fileName.endsWith(s: QLatin1String("html"))
803 && !fileName.endsWith(s: QLatin1String("htm")))
804 continue;
805 QFile htmlFile(fileName);
806 if (!htmlFile.open(flags: QIODevice::ReadOnly)) {
807 emit warning(msg: tr(s: "File \"%1\" cannot be opened.").arg(a: fileName));
808 continue;
809 }
810 const QRegExp linkPattern(QLatin1String("<(?:a href|img src)=\"?([^#\">]+)[#\">]"));
811 QTextStream stream(&htmlFile);
812 const QString codec = QHelpGlobal::codecFromData(data: htmlFile.read(maxlen: 1000));
813 stream.setCodec(QTextCodec::codecForName(name: codec.toLatin1().constData()));
814 const QString &content = stream.readAll();
815 QStringList invalidLinks;
816 for (int pos = linkPattern.indexIn(str: content); pos != -1;
817 pos = linkPattern.indexIn(str: content, offset: pos + 1)) {
818 const QString &linkedFileName = linkPattern.cap(nth: 1);
819 if (linkedFileName.contains(s: QLatin1String("://")))
820 continue;
821 const QString &curDir = QFileInfo(fileName).dir().path();
822 const QString &canonicalLinkedFileName =
823 QFileInfo(curDir + QDir::separator() + linkedFileName).canonicalFilePath();
824 if (!files.contains(value: canonicalLinkedFileName)
825 && !invalidLinks.contains(str: canonicalLinkedFileName)) {
826 emit warning(msg: tr(s: "File \"%1\" contains an invalid link to file \"%2\"").
827 arg(a: fileName).arg(a: linkedFileName));
828 allLinksOk = false;
829 invalidLinks.append(t: canonicalLinkedFileName);
830 }
831 }
832 }
833
834 if (!allLinksOk)
835 m_error = tr(s: "Invalid links in HTML files.");
836 return allLinksOk;
837}
838
839//////////////////////////////
840
841HelpGenerator::HelpGenerator(bool silent)
842{
843 m_private = new HelpGeneratorPrivate(this);
844 if (!silent) {
845 connect(sender: m_private, signal: &HelpGeneratorPrivate::statusChanged,
846 receiver: this, slot: &HelpGenerator::printStatus);
847 }
848 connect(sender: m_private, signal: &HelpGeneratorPrivate::warning,
849 receiver: this, slot: &HelpGenerator::printWarning);
850}
851
852bool HelpGenerator::generate(QHelpProjectData *helpData,
853 const QString &outputFileName)
854{
855 return m_private->generate(helpData, outputFileName);
856}
857
858bool HelpGenerator::checkLinks(const QHelpProjectData &helpData)
859{
860 return m_private->checkLinks(helpData);
861}
862
863QString HelpGenerator::error() const
864{
865 return m_private->error();
866}
867
868void HelpGenerator::printStatus(const QString &msg)
869{
870 puts(qPrintable(msg));
871}
872
873void HelpGenerator::printWarning(const QString &msg)
874{
875 puts(qPrintable(tr("Warning: %1").arg(msg)));
876}
877
878QT_END_NAMESPACE
879
880#include "helpgenerator.moc"
881

source code of qttools/src/assistant/qhelpgenerator/helpgenerator.cpp