| 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 "../shared/collectionconfiguration.h" | 
| 5 | #include "helpgenerator.h" | 
| 6 | #include "collectionconfigreader.h" | 
| 7 | #include "qhelpprojectdata_p.h" | 
| 8 |  | 
| 9 | #include <QtCore/QBuffer> | 
| 10 | #include <QtCore/QDataStream> | 
| 11 | #include <QtCore/QDir> | 
| 12 | #include <QtCore/QFileInfo> | 
| 13 | #include <QtCore/QLibraryInfo> | 
| 14 | #include <QtCore/QRegularExpression> | 
| 15 | #include <QtCore/QTranslator> | 
| 16 |  | 
| 17 | #include <QtGui/QGuiApplication> | 
| 18 |  | 
| 19 | #include <QtHelp/QHelpEngineCore> | 
| 20 |  | 
| 21 |  | 
| 22 | QT_USE_NAMESPACE | 
| 23 |  | 
| 24 | class QHG { | 
| 25 |     Q_DECLARE_TR_FUNCTIONS(QHelpGenerator) | 
| 26 | }; | 
| 27 |  | 
| 28 | static const char QHP[]  = "qhp" ; | 
| 29 | static const char QCH[]  = "qch" ; | 
| 30 |  | 
| 31 | static const char QHCP[] = "qhcp" ; | 
| 32 | static const char QHC[]  = "qhc" ; | 
| 33 |  | 
| 34 | namespace { | 
| 35 |     QString absoluteFilePath(const QString &basePath, const QString &fileName) | 
| 36 |     { | 
| 37 |         return QDir(basePath).absoluteFilePath(fileName); | 
| 38 |     } | 
| 39 | } | 
| 40 |  | 
| 41 | int generateCollectionFile(const QByteArray &data, const QString &basePath, const QString outputFile) | 
| 42 | { | 
| 43 |     fputs(qPrintable(QHG::tr("Reading collection config file...\n" )), stdout); | 
| 44 |     CollectionConfigReader config; | 
| 45 |     config.readData(contents: data); | 
| 46 |     if (config.hasError()) { | 
| 47 |         fputs(qPrintable(QHG::tr("Collection config file error: %1\n" ) | 
| 48 |                          .arg(config.errorString())), stderr); | 
| 49 |         return 1; | 
| 50 |     } | 
| 51 |  | 
| 52 |     const QMap<QString, QString> &filesToGenerate = config.filesToGenerate(); | 
| 53 |     for (auto it = filesToGenerate.cbegin(), end = filesToGenerate.cend(); it != end; ++it) { | 
| 54 |         fputs(qPrintable(QHG::tr("Generating help for %1...\n" ).arg(it.key())), stdout); | 
| 55 |         QHelpProjectData helpData; | 
| 56 |         if (!helpData.readData(fileName: absoluteFilePath(basePath, fileName: it.key()))) { | 
| 57 |             fprintf(stderr, format: "%s\n" , qPrintable(helpData.errorMessage())); | 
| 58 |             return 1; | 
| 59 |         } | 
| 60 |  | 
| 61 |         HelpGenerator helpGenerator; | 
| 62 |         if (!helpGenerator.generate(helpData: &helpData, outputFileName: absoluteFilePath(basePath, fileName: it.value()))) { | 
| 63 |             fprintf(stderr, format: "%s\n" , qPrintable(helpGenerator.error())); | 
| 64 |             return 1; | 
| 65 |         } | 
| 66 |     } | 
| 67 |  | 
| 68 |     fputs(qPrintable(QHG::tr("Creating collection file...\n" )), stdout); | 
| 69 |  | 
| 70 |     QFileInfo colFi(outputFile); | 
| 71 |     if (colFi.exists()) { | 
| 72 |         if (!colFi.dir().remove(fileName: colFi.fileName())) { | 
| 73 |             fputs(qPrintable(QHG::tr("The file %1 cannot be overwritten.\n" ) | 
| 74 |                              .arg(outputFile)), stderr); | 
| 75 |             return 1; | 
| 76 |         } | 
| 77 |     } | 
| 78 |  | 
| 79 |     QHelpEngineCore helpEngine(outputFile); | 
| 80 |     helpEngine.setReadOnly(false); | 
| 81 |     if (!helpEngine.setupData()) { | 
| 82 |         fprintf(stderr, format: "%s\n" , qPrintable(helpEngine.error())); | 
| 83 |         return 1; | 
| 84 |     } | 
| 85 |  | 
| 86 |     for (const QString &file : config.filesToRegister()) { | 
| 87 |         if (!helpEngine.registerDocumentation(documentationFileName: absoluteFilePath(basePath, fileName: file))) { | 
| 88 |             fprintf(stderr, format: "%s\n" , qPrintable(helpEngine.error())); | 
| 89 |             return 1; | 
| 90 |         } | 
| 91 |     } | 
| 92 |     if (!config.filesToRegister().isEmpty()) { | 
| 93 |         if (Q_UNLIKELY(qEnvironmentVariableIsSet("SOURCE_DATE_EPOCH" ))) { | 
| 94 |             QDateTime dt; | 
| 95 |             dt.setTimeZone(toZone: QTimeZone::UTC); | 
| 96 |             dt.setSecsSinceEpoch(qEnvironmentVariableIntValue(varName: "SOURCE_DATE_EPOCH" )); | 
| 97 |             CollectionConfiguration::updateLastRegisterTime(helpEngine, dt); | 
| 98 |         } else { | 
| 99 |             CollectionConfiguration::updateLastRegisterTime(helpEngine); | 
| 100 |         } | 
| 101 |     } | 
| 102 |  | 
| 103 |     if (!config.title().isEmpty()) | 
| 104 |         CollectionConfiguration::setWindowTitle(helpEngine, windowTitle: config.title()); | 
| 105 |  | 
| 106 |     if (!config.homePage().isEmpty()) { | 
| 107 |         CollectionConfiguration::setDefaultHomePage(helpEngine, | 
| 108 |             page: config.homePage()); | 
| 109 |     } | 
| 110 |  | 
| 111 |     if (!config.startPage().isEmpty()) { | 
| 112 |         CollectionConfiguration::setLastShownPages(helpEngine, | 
| 113 |             lastShownPages: QStringList(config.startPage())); | 
| 114 |     } | 
| 115 |  | 
| 116 |     if (!config.currentFilter().isEmpty()) { | 
| 117 |         helpEngine.setCurrentFilter(config.currentFilter()); | 
| 118 |     } | 
| 119 |  | 
| 120 |     if (!config.cacheDirectory().isEmpty()) { | 
| 121 |         CollectionConfiguration::setCacheDir(helpEngine, cacheDir: config.cacheDirectory(), | 
| 122 |             relativeToCollection: config.cacheDirRelativeToCollection()); | 
| 123 |     } | 
| 124 |  | 
| 125 |     CollectionConfiguration::setFilterFunctionalityEnabled(helpEngine, | 
| 126 |         enabled: config.enableFilterFunctionality()); | 
| 127 |     CollectionConfiguration::setFilterToolbarVisible(helpEngine, | 
| 128 |         visible: !config.hideFilterFunctionality()); | 
| 129 |     CollectionConfiguration::setDocumentationManagerEnabled(helpEngine, | 
| 130 |         enabled: config.enableDocumentationManager()); | 
| 131 |     CollectionConfiguration::setAddressBarEnabled(helpEngine, | 
| 132 |         enabled: config.enableAddressBar()); | 
| 133 |     CollectionConfiguration::setAddressBarVisible(helpEngine, | 
| 134 |          visible: !config.hideAddressBar()); | 
| 135 |     uint time = QDateTime::currentMSecsSinceEpoch() / 1000; | 
| 136 |     if (Q_UNLIKELY(qEnvironmentVariableIsSet("SOURCE_DATE_EPOCH" ))) | 
| 137 |         time = qEnvironmentVariableIntValue(varName: "SOURCE_DATE_EPOCH" ); | 
| 138 |     CollectionConfiguration::setCreationTime(helpEngine, time); | 
| 139 |     CollectionConfiguration::setFullTextSearchFallbackEnabled(helpEngine, | 
| 140 |         on: config.fullTextSearchFallbackEnabled()); | 
| 141 |  | 
| 142 |     if (!config.applicationIcon().isEmpty()) { | 
| 143 |         QFile icon(absoluteFilePath(basePath, fileName: config.applicationIcon())); | 
| 144 |         if (!icon.open(flags: QIODevice::ReadOnly)) { | 
| 145 |             fputs(qPrintable(QHG::tr("Cannot open %1.\n" ).arg(icon.fileName())), stderr); | 
| 146 |             return 1; | 
| 147 |         } | 
| 148 |         CollectionConfiguration::setApplicationIcon(helpEngine, icon: icon.readAll()); | 
| 149 |     } | 
| 150 |  | 
| 151 |     if (config.aboutMenuTexts().size()) { | 
| 152 |         QByteArray ba; | 
| 153 |         QDataStream s(&ba, QIODevice::WriteOnly); | 
| 154 |         const QMap<QString, QString> & = config.aboutMenuTexts(); | 
| 155 |         for (auto it = aboutMenuTexts.cbegin(), end = aboutMenuTexts.cend(); it != end; ++it) | 
| 156 |             s << it.key() << it.value(); | 
| 157 |         CollectionConfiguration::setAboutMenuTexts(helpEngine, texts: ba); | 
| 158 |     } | 
| 159 |  | 
| 160 |     if (!config.aboutIcon().isEmpty()) { | 
| 161 |         QFile icon(absoluteFilePath(basePath, fileName: config.aboutIcon())); | 
| 162 |         if (!icon.open(flags: QIODevice::ReadOnly)) { | 
| 163 |             fputs(qPrintable(QHG::tr("Cannot open %1.\n" ).arg(icon.fileName())), stderr); | 
| 164 |             return 1; | 
| 165 |         } | 
| 166 |         CollectionConfiguration::setAboutIcon(helpEngine, icon: icon.readAll()); | 
| 167 |     } | 
| 168 |  | 
| 169 |     if (config.aboutTextFiles().size()) { | 
| 170 |         QByteArray ba; | 
| 171 |         QDataStream s(&ba, QIODevice::WriteOnly); | 
| 172 |         QMap<QString, QByteArray> imgData; | 
| 173 |  | 
| 174 |         QRegularExpression srcRegExp(QLatin1String("src=(\"(.+)\"|([^\"\\s]+)).*>" ), QRegularExpression::InvertedGreedinessOption); | 
| 175 |         QRegularExpression imgRegExp(QLatin1String("(<img[^>]+>)" ), QRegularExpression::InvertedGreedinessOption); | 
| 176 |  | 
| 177 |         const QMap<QString, QString> & = config.aboutTextFiles(); | 
| 178 |         for (auto it = aboutMenuTexts.cbegin(), end = aboutMenuTexts.cend(); it != end; ++it) { | 
| 179 |             s << it.key(); | 
| 180 |             QFileInfo fi(absoluteFilePath(basePath, fileName: it.value())); | 
| 181 |             QFile f(fi.absoluteFilePath()); | 
| 182 |             if (!f.open(flags: QIODevice::ReadOnly)) { | 
| 183 |                 fputs(qPrintable(QHG::tr("Cannot open %1.\n" ).arg(f.fileName())), stderr); | 
| 184 |                 return 1; | 
| 185 |             } | 
| 186 |             QByteArray data = f.readAll(); | 
| 187 |             s << data; | 
| 188 |  | 
| 189 |             QString contents = QString::fromUtf8(ba: data); | 
| 190 |             int pos = 0; | 
| 191 |             QRegularExpressionMatch match; | 
| 192 |             while ((match = imgRegExp.match(subject: contents, offset: pos)).hasMatch()) { | 
| 193 |                 QString imgTag = match.captured(nth: 1); | 
| 194 |                 pos = match.capturedEnd(); | 
| 195 |  | 
| 196 |                 if ((match = srcRegExp.match(subject: imgTag)).hasMatch()) { | 
| 197 |                     QString src = match.captured(nth: 2); | 
| 198 |                     if (src.isEmpty()) | 
| 199 |                         src = match.captured(nth: 3); | 
| 200 |  | 
| 201 |                     QFile img(fi.absolutePath() + QDir::separator() + src); | 
| 202 |                     if (img.open(flags: QIODevice::ReadOnly)) { | 
| 203 |                         if (!imgData.contains(key: src)) | 
| 204 |                             imgData.insert(key: src, value: img.readAll()); | 
| 205 |                     } else { | 
| 206 |                         fputs(qPrintable(QHG::tr("Cannot open referenced image file %1.\n" ) | 
| 207 |                                          .arg(img.fileName())), stderr); | 
| 208 |                     } | 
| 209 |                 } | 
| 210 |             } | 
| 211 |         } | 
| 212 |         CollectionConfiguration::setAboutTexts(helpEngine, texts: ba); | 
| 213 |         if (imgData.size()) { | 
| 214 |             QByteArray imageData; | 
| 215 |             QBuffer buffer(&imageData); | 
| 216 |             buffer.open(openMode: QIODevice::WriteOnly); | 
| 217 |             QDataStream out(&buffer); | 
| 218 |             out << imgData; | 
| 219 |             CollectionConfiguration::setAboutImages(helpEngine, images: imageData); | 
| 220 |         } | 
| 221 |     } | 
| 222 |     return 0; | 
| 223 | } | 
| 224 |  | 
| 225 | int main(int argc, char *argv[]) | 
| 226 | { | 
| 227 |     QString error; | 
| 228 |     QString outputFile; | 
| 229 |     QString inputFile; | 
| 230 |     QString basePath; | 
| 231 |     bool showHelp = false; | 
| 232 |     bool showVersion = false; | 
| 233 |     bool checkLinks = false; | 
| 234 |     bool silent = false; | 
| 235 |  | 
| 236 |     // don't require a window manager even though we're a QGuiApplication | 
| 237 |     qputenv(varName: "QT_QPA_PLATFORM" , QByteArrayLiteral("minimal" )); | 
| 238 |  | 
| 239 |     QGuiApplication app(argc, argv); | 
| 240 | #ifndef Q_OS_WIN32 | 
| 241 |     QTranslator translator; | 
| 242 |     QTranslator qtTranslator; | 
| 243 |     QTranslator qt_helpTranslator; | 
| 244 |     QString sysLocale = QLocale::system().name(); | 
| 245 |     QString resourceDir = QLibraryInfo::path(p: QLibraryInfo::TranslationsPath); | 
| 246 |     if (translator.load(filename: QLatin1String("assistant_" ) + sysLocale, directory: resourceDir) | 
| 247 |         && qtTranslator.load(filename: QLatin1String("qt_" ) + sysLocale, directory: resourceDir) | 
| 248 |         && qt_helpTranslator.load(filename: QLatin1String("qt_help_" ) + sysLocale, directory: resourceDir)) { | 
| 249 |         app.installTranslator(messageFile: &translator); | 
| 250 |         app.installTranslator(messageFile: &qtTranslator); | 
| 251 |         app.installTranslator(messageFile: &qt_helpTranslator); | 
| 252 |     } | 
| 253 | #endif // Q_OS_WIN32 | 
| 254 |  | 
| 255 |     for (int i = 1; i < argc; ++i) { | 
| 256 |         const QString arg = QString::fromLocal8Bit(ba: argv[i]); | 
| 257 |         if (arg == QLatin1String("-o" )) { | 
| 258 |             if (++i < argc) { | 
| 259 |                 QFileInfo fi(QString::fromLocal8Bit(ba: argv[i])); | 
| 260 |                 outputFile = fi.absoluteFilePath(); | 
| 261 |             } else { | 
| 262 |                 error = QHG::tr(sourceText: "Missing output file name." ); | 
| 263 |             } | 
| 264 |         } else if (arg == QLatin1String("-v" )) { | 
| 265 |             showVersion = true; | 
| 266 |         } else if (arg == QLatin1String("-h" )) { | 
| 267 |             showHelp = true; | 
| 268 |         } else if (arg == QLatin1String("-c" )) { | 
| 269 |             checkLinks = true; | 
| 270 |         } else if (arg == QLatin1String("-s" )) { | 
| 271 |             silent = true; | 
| 272 |         } else { | 
| 273 |             const QFileInfo fi(arg); | 
| 274 |             inputFile = fi.absoluteFilePath(); | 
| 275 |             basePath = fi.absolutePath(); | 
| 276 |         } | 
| 277 |     } | 
| 278 |  | 
| 279 |     if (showVersion) { | 
| 280 |         fputs(qPrintable(QHG::tr("Qt Help Generator version 1.0 (Qt %1)\n" ) | 
| 281 |                          .arg(QT_VERSION_STR)), stdout); | 
| 282 |         return 0; | 
| 283 |     } | 
| 284 |  | 
| 285 |     enum InputType { | 
| 286 |         InputQhp, | 
| 287 |         InputQhcp, | 
| 288 |         InputUnknown | 
| 289 |     }; | 
| 290 |  | 
| 291 |     InputType inputType = InputUnknown; | 
| 292 |  | 
| 293 |     if (!showHelp) { | 
| 294 |         if (inputFile.isEmpty()) { | 
| 295 |             error = QHG::tr(sourceText: "Missing input file name." ); | 
| 296 |         } else { | 
| 297 |             const QFileInfo fi(inputFile); | 
| 298 |             if (fi.suffix() == QHP) | 
| 299 |                 inputType = InputQhp; | 
| 300 |             else if (fi.suffix() == QHCP) | 
| 301 |                 inputType = InputQhcp; | 
| 302 |  | 
| 303 |             if (inputType == InputUnknown) | 
| 304 |                 error = QHG::tr(sourceText: "Unknown input file type." ); | 
| 305 |         } | 
| 306 |     } | 
| 307 |  | 
| 308 |     const QString help = QHG::tr(sourceText: "\nUsage:\n\n"  | 
| 309 |         "qhelpgenerator <file> [options]\n\n"  | 
| 310 |         "  -o <output-file>       Generates a Qt compressed help\n"  | 
| 311 |         "                         called <output-file> (*.qch) for the\n"  | 
| 312 |         "                         Qt help project <file> (*.qhp).\n"  | 
| 313 |         "                         Generates a Qt help collection\n"  | 
| 314 |         "                         called <output-file> (*.qhc) for the\n"  | 
| 315 |         "                         Qt help collection project <file> (*.qhcp).\n"  | 
| 316 |         "                         If this option is not specified\n"  | 
| 317 |         "                         a default name will be used\n"  | 
| 318 |         "                         (*.qch for *.qhp and *.qhc for *.qhcp).\n"  | 
| 319 |         "  -c                     Checks whether all links in HTML files\n"  | 
| 320 |         "                         point to files in this help project.\n"  | 
| 321 |         "  -s                     Suppresses status messages.\n"  | 
| 322 |         "  -v                     Displays the version of \n"  | 
| 323 |         "                         qhelpgenerator.\n\n" ); | 
| 324 |  | 
| 325 |     if (showHelp) { | 
| 326 |         fputs(qPrintable(help), stdout); | 
| 327 |         return 0; | 
| 328 |     } else if (!error.isEmpty()) { | 
| 329 |         fprintf(stderr, format: "%s\n\n%s" , qPrintable(error), qPrintable(help)); | 
| 330 |         return 1; | 
| 331 |     } | 
| 332 |  | 
| 333 |     // detect input file type (qhp or qhcp) | 
| 334 |  | 
| 335 |     QFile file(inputFile); | 
| 336 |     if (!file.open(flags: QIODevice::ReadOnly)) { | 
| 337 |         fputs(qPrintable(QHG::tr("Could not open %1.\n" ).arg(inputFile)), stderr); | 
| 338 |         return 1; | 
| 339 |     } | 
| 340 |  | 
| 341 |     const QString outputExtension = inputType == InputQhp ? QCH : QHC; | 
| 342 |  | 
| 343 |     if (outputFile.isEmpty()) { | 
| 344 |         if (inputType == InputQhcp || !checkLinks) { | 
| 345 |             QFileInfo fi(inputFile); | 
| 346 |             outputFile = basePath + QDir::separator() | 
| 347 |                              + fi.baseName() + QLatin1Char('.') + outputExtension; | 
| 348 |         } | 
| 349 |     } else { | 
| 350 |         // check if the output dir exists -- create if it doesn't | 
| 351 |         QFileInfo fi(outputFile); | 
| 352 |         QDir parentDir = fi.dir(); | 
| 353 |         if (!parentDir.exists()) { | 
| 354 |             if (!parentDir.mkpath(dirPath: QLatin1String("." ))) { | 
| 355 |                 fputs(qPrintable(QHG::tr("Could not create output directory: %1\n" ) | 
| 356 |                                  .arg(parentDir.path())), stderr); | 
| 357 |             } | 
| 358 |         } | 
| 359 |     } | 
| 360 |  | 
| 361 |     if (inputType == InputQhp) { | 
| 362 |         QHelpProjectData *helpData = new QHelpProjectData(); | 
| 363 |         if (!helpData->readData(fileName: inputFile)) { | 
| 364 |             fprintf(stderr, format: "%s\n" , qPrintable(helpData->errorMessage())); | 
| 365 |             return 1; | 
| 366 |         } | 
| 367 |  | 
| 368 |         HelpGenerator generator(silent); | 
| 369 |         bool success = true; | 
| 370 |         if (checkLinks) | 
| 371 |             success = generator.checkLinks(helpData: *helpData); | 
| 372 |         if (success && !outputFile.isEmpty()) | 
| 373 |             success = generator.generate(helpData, outputFileName: outputFile); | 
| 374 |         delete helpData; | 
| 375 |         if (!success) { | 
| 376 |             fprintf(stderr, format: "%s\n" , qPrintable(generator.error())); | 
| 377 |             return 1; | 
| 378 |         } | 
| 379 |     } else { | 
| 380 |         const QByteArray data = file.readAll(); | 
| 381 |         return generateCollectionFile(data, basePath, outputFile); | 
| 382 |  | 
| 383 |     } | 
| 384 |  | 
| 385 |     return 0; | 
| 386 | } | 
| 387 |  |