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 | |
59 | QT_BEGIN_NAMESPACE |
60 | |
61 | class HelpGeneratorPrivate : public QObject |
62 | { |
63 | Q_OBJECT |
64 | |
65 | public: |
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 | |
73 | Q_SIGNALS: |
74 | void statusChanged(const QString &msg); |
75 | void progressChanged(double progress); |
76 | void warning(const QString &msg); |
77 | |
78 | private: |
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 | */ |
125 | bool 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 | |
214 | void 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 | |
235 | void 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 | |
244 | void 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 | |
254 | void 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 | */ |
266 | QString HelpGeneratorPrivate::error() const |
267 | { |
268 | return m_error; |
269 | } |
270 | |
271 | bool 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 | |
348 | bool 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 | |
373 | bool 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 | |
424 | bool 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 | |
569 | bool 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 | |
629 | bool 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 | |
708 | bool 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 | |
742 | bool 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 | |
762 | bool 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 | |
776 | bool 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 | |
841 | HelpGenerator::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 | |
852 | bool HelpGenerator::generate(QHelpProjectData *helpData, |
853 | const QString &outputFileName) |
854 | { |
855 | return m_private->generate(helpData, outputFileName); |
856 | } |
857 | |
858 | bool HelpGenerator::checkLinks(const QHelpProjectData &helpData) |
859 | { |
860 | return m_private->checkLinks(helpData); |
861 | } |
862 | |
863 | QString HelpGenerator::error() const |
864 | { |
865 | return m_private->error(); |
866 | } |
867 | |
868 | void HelpGenerator::printStatus(const QString &msg) |
869 | { |
870 | puts(qPrintable(msg)); |
871 | } |
872 | |
873 | void HelpGenerator::printWarning(const QString &msg) |
874 | { |
875 | puts(qPrintable(tr("Warning: %1" ).arg(msg))); |
876 | } |
877 | |
878 | QT_END_NAMESPACE |
879 | |
880 | #include "helpgenerator.moc" |
881 | |