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 | |
25 | QT_BEGIN_NAMESPACE |
26 | |
27 | class HelpGeneratorPrivate : public QObject |
28 | { |
29 | Q_OBJECT |
30 | |
31 | public: |
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 | |
39 | Q_SIGNALS: |
40 | void statusChanged(const QString &msg); |
41 | void progressChanged(double progress); |
42 | void warning(const QString &msg); |
43 | |
44 | private: |
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 | */ |
91 | bool 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 | |
180 | void 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 | |
201 | void 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 | |
210 | void 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 | |
220 | void 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 | */ |
232 | QString HelpGeneratorPrivate::error() const |
233 | { |
234 | return m_error; |
235 | } |
236 | |
237 | bool 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 | |
314 | bool 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 | |
339 | bool 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 | |
390 | bool 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 | |
537 | bool 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 | |
597 | bool 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 | |
676 | bool 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 | |
710 | bool 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 | |
730 | bool 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 | |
744 | bool 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 | |
812 | HelpGenerator::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 | |
823 | bool HelpGenerator::generate(QHelpProjectData *helpData, |
824 | const QString &outputFileName) |
825 | { |
826 | return m_private->generate(helpData, outputFileName); |
827 | } |
828 | |
829 | bool HelpGenerator::checkLinks(const QHelpProjectData &helpData) |
830 | { |
831 | return m_private->checkLinks(helpData); |
832 | } |
833 | |
834 | QString HelpGenerator::error() const |
835 | { |
836 | return m_private->error(); |
837 | } |
838 | |
839 | void HelpGenerator::printStatus(const QString &msg) |
840 | { |
841 | puts(qPrintable(msg)); |
842 | } |
843 | |
844 | void HelpGenerator::printWarning(const QString &msg) |
845 | { |
846 | puts(qPrintable(tr("Warning: %1" ).arg(msg))); |
847 | } |
848 | |
849 | QT_END_NAMESPACE |
850 | |
851 | #include "helpgenerator.moc" |
852 | |