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

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