| 1 | // Copyright (C) 2016 The Qt Company Ltd. | 
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 | 
| 3 |  | 
| 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: {}, 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(ch: 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 |  |