1 /****************************************************************************
2 **
3 ** Copyright (C) 2015 The Qt Company Ltd.
4 ** Contact: http://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 http://www.qt.io/terms-conditions. For further
15 ** information use the contact form at http://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 2.1 or version 3 as published by the Free
20 ** Software Foundation and appearing in the file LICENSE.LGPLv21 and
21 ** LICENSE.LGPLv3 included in the packaging of this file. Please review the
22 ** following information to ensure the GNU Lesser General Public License
23 ** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
24 ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
25 **
26 ** As a special exception, The Qt Company gives you certain additional
27 ** rights. These rights are described in The Qt Company LGPL Exception
28 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
29 **
30 ** GNU General Public License Usage
31 ** Alternatively, this file may be used under the terms of the GNU
32 ** General Public License version 3.0 as published by the Free Software
33 ** Foundation and appearing in the file LICENSE.GPL included in the
34 ** packaging of this file.  Please review the following information to
35 ** ensure the GNU General Public License version 3.0 requirements will be
36 ** met: http://www.gnu.org/copyleft/gpl.html.
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41 
42 #include "../shared/collectionconfiguration.h"
43 #include "../shared/helpgenerator.h"
44 
45 #include <private/qhelpgenerator_p.h>
46 #include <private/qhelpprojectdata_p.h>
47 
48 #include <QtCore/QCoreApplication>
49 #include <QtCore/QDir>
50 #include <QtCore/QMap>
51 #include <QtCore/QFileInfo>
52 #include <QtCore/QDateTime>
53 #include <QtCore/QBuffer>
54 #include <QtCore/QTranslator>
55 #include <QtCore/QLocale>
56 #include <QtCore/QLibraryInfo>
57 #include <QtHelp/QHelpEngineCore>
58 #include <QtXml/QXmlStreamReader>
59 
60 
61 QT_USE_NAMESPACE
62 
63 class QCG {
64     Q_DECLARE_TR_FUNCTIONS(QCollectionGenerator)
65 };
66 
67 class CollectionConfigReader : public QXmlStreamReader
68 {
69 public:
70     void readData(const QByteArray &contents);
71 
title() const72     QString title() const { return m_title; }
homePage() const73     QString homePage() const { return m_homePage; }
startPage() const74     QString startPage() const { return m_startPage; }
applicationIcon() const75     QString applicationIcon() const { return m_applicationIcon; }
currentFilter() const76     QString currentFilter() const { return m_currentFilter; }
enableFilterFunctionality() const77     bool enableFilterFunctionality() const
78         { return m_enableFilterFunctionality; }
hideFilterFunctionality() const79     bool hideFilterFunctionality() const
80         { return m_hideFilterFunctionality; }
enableAddressBar() const81         bool enableAddressBar() const { return m_enableAddressBar; }
hideAddressBar() const82     bool hideAddressBar() const { return m_hideAddressBar; }
enableDocumentationManager() const83     bool enableDocumentationManager() const
84         { return m_enableDocumentationManager; }
85 
aboutMenuTexts() const86     QMap<QString, QString> aboutMenuTexts() const
87         { return m_aboutMenuTexts; }
aboutIcon() const88     QString aboutIcon() const { return m_aboutIcon; }
aboutTextFiles() const89     QMap<QString, QString> aboutTextFiles() const
90         { return m_aboutTextFiles; }
91 
filesToGenerate() const92     QMap<QString, QString> filesToGenerate() const
93         { return m_filesToGenerate; }
94 
filesToRegister() const95     QStringList filesToRegister() const { return m_filesToRegister; }
96 
cacheDirectory() const97     QString cacheDirectory() const { return m_cacheDirectory; }
cacheDirRelativeToCollection() const98     bool cacheDirRelativeToCollection() const { return m_cacheDirRelativeToCollection; }
99 
fullTextSearchFallbackEnabled() const100     bool fullTextSearchFallbackEnabled() const {
101         return m_enableFullTextSearchFallback;
102     }
103 
104 private:
105     void raiseErrorWithLine();
106     void readConfig();
107     void readAssistantSettings();
108     void readMenuTexts();
109     void readAboutDialog();
110     void readDocFiles();
111     void readGenerate();
112     void readFiles();
113     void readRegister();
114 
115     QString m_title;
116     QString m_homePage;
117     QString m_startPage;
118     QString m_applicationIcon;
119     QString m_currentFilter;
120     bool m_enableFilterFunctionality;
121     bool m_hideFilterFunctionality;
122     bool m_enableAddressBar;
123     bool m_hideAddressBar;
124     bool m_enableDocumentationManager;
125     QMap<QString, QString> m_aboutMenuTexts;
126     QString m_aboutIcon;
127     QMap<QString, QString> m_aboutTextFiles;
128     QMap<QString, QString> m_filesToGenerate;
129     QStringList m_filesToRegister;
130     QString m_cacheDirectory;
131     bool m_cacheDirRelativeToCollection;
132     bool m_enableFullTextSearchFallback;
133 };
134 
raiseErrorWithLine()135 void CollectionConfigReader::raiseErrorWithLine()
136 {
137     raiseError(QCG::tr("Unknown token at line %1.").arg(lineNumber()));
138 }
139 
readData(const QByteArray & contents)140 void CollectionConfigReader::readData(const QByteArray &contents)
141 {
142     m_enableFilterFunctionality = true;
143     m_hideFilterFunctionality = true;
144     m_enableAddressBar = true;
145     m_hideAddressBar = true;
146     m_enableDocumentationManager = true;
147     m_enableFullTextSearchFallback = false;
148 
149     addData(contents);
150     while (!atEnd()) {
151         readNext();
152         if (isStartElement()) {
153             if (name() == QLatin1String("QHelpCollectionProject")
154                 && attributes().value(QLatin1String("version")) == QLatin1String("1.0"))
155                 readConfig();
156             else
157                 raiseError(QCG::tr("Unknown token at line %1. "
158                                    "Expected \"QtHelpCollectionProject\".")
159                            .arg(lineNumber()));
160         }
161     }
162 }
163 
readConfig()164 void CollectionConfigReader::readConfig()
165 {
166     bool ok = false;
167     while (!atEnd()) {
168         readNext();
169         if (isStartElement()) {
170             if (name() == QLatin1String("assistant"))
171                 readAssistantSettings();
172             else if (name() == QLatin1String("docFiles"))
173                 readDocFiles();
174             else
175                 raiseErrorWithLine();
176         } else if (isEndElement() && name() == QLatin1String("QHelpCollectionProject")) {
177             ok = true;
178         }
179     }
180     if (!ok && !hasError())
181         raiseError(QCG::tr("Missing end tags."));
182 }
183 
readAssistantSettings()184 void CollectionConfigReader::readAssistantSettings()
185 {
186     while (!atEnd()) {
187         readNext();
188         if (isStartElement()) {
189             if (name() == QLatin1String("title")) {
190                 m_title = readElementText();
191             } else if (name() == QLatin1String("homePage")) {
192                 m_homePage = readElementText();
193             } else if (name() == QLatin1String("startPage")) {
194                 m_startPage = readElementText();
195             } else if (name() == QLatin1String("currentFilter")) {
196                 m_currentFilter = readElementText();
197             } else if (name() == QLatin1String("applicationIcon")) {
198                 m_applicationIcon = readElementText();
199             } else if (name() == QLatin1String("enableFilterFunctionality")) {
200                 if (attributes().value(QLatin1String("visible")) == QLatin1String("true"))
201                     m_hideFilterFunctionality = false;
202                 if (readElementText() == QLatin1String("false"))
203                     m_enableFilterFunctionality = false;
204             } else if (name() == QLatin1String("enableDocumentationManager")) {
205                 if (readElementText() == QLatin1String("false"))
206                     m_enableDocumentationManager = false;
207             } else if (name() == QLatin1String("enableAddressBar")) {
208                 if (attributes().value(QLatin1String("visible")) == QLatin1String("true"))
209                     m_hideAddressBar = false;
210                 if (readElementText() == QLatin1String("false"))
211                     m_enableAddressBar = false;
212             } else if (name() == QLatin1String("aboutMenuText")) {
213                 readMenuTexts();
214             } else if (name() == QLatin1String("aboutDialog")) {
215                 readAboutDialog();
216             } else if (name() == "cacheDirectory") {
217                 m_cacheDirRelativeToCollection =
218                     attributes().value(QLatin1String("base"))
219                     == QLatin1String("collection");
220                 m_cacheDirectory = readElementText();
221             } else if (name() == QLatin1String("enableFullTextSearchFallback")) {
222                 if (readElementText() == QLatin1String("true"))
223                     m_enableFullTextSearchFallback = true;
224             } else {
225                 raiseErrorWithLine();
226             }
227         } else if (isEndElement() && name() == QLatin1String("assistant")) {
228             break;
229         }
230     }
231 }
232 
readMenuTexts()233 void CollectionConfigReader::readMenuTexts()
234 {
235     while (!atEnd())
236     {
237         readNext();
238         if (isStartElement()) {
239             if (name() == QLatin1String("text")) {
240                 QString lang = attributes().value(QLatin1String("language")).toString();
241                 if (lang.isEmpty())
242                     lang = QLatin1String("default");
243                 m_aboutMenuTexts.insert(lang, readElementText());
244             } else {
245                 raiseErrorWithLine();
246             }
247         } else if (isEndElement() && name() == QLatin1String("aboutMenuText")) {
248             break;
249         }
250     }
251 }
252 
readAboutDialog()253 void CollectionConfigReader::readAboutDialog()
254 {
255     while (!atEnd())
256     {
257         readNext();
258         if (isStartElement()) {
259             if (name() == QLatin1String("file")) {
260                 QString lang = attributes().value(QLatin1String("language")).toString();
261                 if (lang.isEmpty())
262                     lang = QLatin1String("default");
263                 m_aboutTextFiles.insert(lang, readElementText());
264             } else if (name() == QLatin1String("icon")) {
265                 m_aboutIcon = readElementText();
266             } else {
267                 raiseErrorWithLine();
268             }
269         } else if (isEndElement() && name() == QLatin1String("aboutDialog")) {
270             break;
271         }
272     }
273 }
274 
readDocFiles()275 void CollectionConfigReader::readDocFiles()
276 {
277     while (!atEnd()) {
278         readNext();
279         if (isStartElement()) {
280             if (name() == QLatin1String("generate")) {
281                 readGenerate();
282             } else if (name() == QLatin1String("register")) {
283                 readRegister();
284             } else {
285                 raiseErrorWithLine();
286             }
287         } else if (isEndElement() && name() == QLatin1String("docFiles")) {
288             break;
289         }
290     }
291 }
292 
readGenerate()293 void CollectionConfigReader::readGenerate()
294 {
295     while (!atEnd()) {
296         readNext();
297         if (isStartElement()) {
298             if (name() == QLatin1String("file"))
299                 readFiles();
300             else
301                 raiseErrorWithLine();
302         } else if (isEndElement() && name() == QLatin1String("generate")) {
303             break;
304         }
305     }
306 }
307 
readFiles()308 void CollectionConfigReader::readFiles()
309 {
310     QString input;
311     QString output;
312     while (!atEnd()) {
313         readNext();
314         if (isStartElement()) {
315             if (name() == QLatin1String("input"))
316                 input = readElementText();
317             else if (name() == QLatin1String("output"))
318                 output = readElementText();
319             else
320                 raiseErrorWithLine();
321         } else if (isEndElement() && name() == QLatin1String("file")) {
322             break;
323         }
324     }
325     if (input.isEmpty() || output.isEmpty()) {
326         raiseError(QCG::tr("Missing input or output file for help file generation."));
327         return;
328     }
329     m_filesToGenerate.insert(input, output);
330 }
331 
readRegister()332 void CollectionConfigReader::readRegister()
333 {
334     while (!atEnd()) {
335         readNext();
336         if (isStartElement()) {
337             if (name() == QLatin1String("file"))
338                 m_filesToRegister.append(readElementText());
339             else
340                 raiseErrorWithLine();
341         } else if (isEndElement() && name() == QLatin1String("register")) {
342             break;
343         }
344     }
345 }
346 
347 namespace {
absoluteFileName(const QString & basePath,const QString & fileName)348     QString absoluteFileName(const QString &basePath, const QString &fileName)
349     {
350         return QFileInfo(fileName).isAbsolute() ?
351             fileName : basePath + QDir::separator() + fileName;
352     }
353 }
354 
main(int argc,char * argv[])355 int main(int argc, char *argv[])
356 {
357     QString error;
358     QString arg;
359     QString collectionFile;
360     QString configFile;
361     QString basePath;
362     bool showHelp = false;
363     bool showVersion = false;
364 
365     QCoreApplication app(argc, argv);
366 #ifndef Q_OS_WIN32
367     QTranslator translator;
368     QTranslator qtTranslator;
369     QTranslator qt_helpTranslator;
370     QString sysLocale = QLocale::system().name();
371     QString resourceDir = QLibraryInfo::location(QLibraryInfo::TranslationsPath);
372     if (translator.load(QLatin1String("assistant_") + sysLocale, resourceDir)
373         && qtTranslator.load(QLatin1String("qt_") + sysLocale, resourceDir)
374         && qt_helpTranslator.load(QLatin1String("qt_help_") + sysLocale, resourceDir)) {
375         app.installTranslator(&translator);
376         app.installTranslator(&qtTranslator);
377         app.installTranslator(&qt_helpTranslator);
378     }
379 #endif // Q_OS_WIN32
380 
381     for (int i=1; i<argc; ++i) {
382         arg = QString::fromLocal8Bit(argv[i]);
383         if (arg == QLatin1String("-o")) {
384             if (++i < argc) {
385                 QFileInfo fi(QString::fromLocal8Bit(argv[i]));
386                 collectionFile = fi.absoluteFilePath();
387             } else {
388                 error = QCG::tr("Missing output file name.");
389             }
390         } else if (arg == QLatin1String("-h")) {
391             showHelp = true;
392         } else if (arg == QLatin1String("-v")) {
393             showVersion = true;
394         } else {
395             QFileInfo fi(arg);
396             configFile = fi.absoluteFilePath();
397             basePath = fi.absolutePath();
398         }
399     }
400 
401     if (showVersion) {
402         fputs(qPrintable(QCG::tr("Qt Collection Generator version 1.0 (Qt %1)\n")
403                 .arg(QT_VERSION_STR)), stdout);
404         return 0;
405     }
406 
407     if (configFile.isEmpty() && !showHelp)
408         error = QCG::tr("Missing collection config file.");
409 
410     QString help = QCG::tr("\nUsage:\n\n"
411         "qcollectiongenerator <collection-config-file> [options]\n\n"
412         "  -o <collection-file>   Generates a collection file\n"
413         "                         called <collection-file>. If\n"
414         "                         this option is not specified\n"
415         "                         a default name will be used.\n"
416         "  -v                     Displays the version of\n"
417         "                         qcollectiongenerator.\n\n");
418 
419     if (showHelp) {
420         fputs(qPrintable(help), stdout);
421         return 0;
422     }else if (!error.isEmpty()) {
423         fprintf(stderr, "%s\n\n%s", qPrintable(error), qPrintable(help));
424         return -1;
425     }
426 
427     QFile file(configFile);
428     if (!file.open(QIODevice::ReadOnly)) {
429         fputs(qPrintable(QCG::tr("Could not open %1.\n").arg(configFile)), stderr);
430         return -1;
431     }
432 
433     if (collectionFile.isEmpty()) {
434         QFileInfo fi(configFile);
435         collectionFile = basePath + QDir::separator()
436             + fi.baseName() + QLatin1String(".qhc");
437     }
438 
439     fputs(qPrintable(QCG::tr("Reading collection config file...\n")), stdout);
440     CollectionConfigReader config;
441     config.readData(file.readAll());
442     if (config.hasError()) {
443         fputs(qPrintable(QCG::tr("Collection config file error: %1\n")
444                          .arg(config.errorString())), stderr);
445         return -1;
446     }
447 
448     QMap<QString, QString>::const_iterator it = config.filesToGenerate().constBegin();
449     while (it != config.filesToGenerate().constEnd()) {
450         fputs(qPrintable(QCG::tr("Generating help for %1...\n").arg(it.key())), stdout);
451         QHelpProjectData helpData;
452         if (!helpData.readData(absoluteFileName(basePath, it.key()))) {
453             fprintf(stderr, "%s\n", qPrintable(helpData.errorMessage()));
454             return -1;
455         }
456 
457         HelpGenerator helpGenerator;
458         if (!helpGenerator.generate(&helpData, absoluteFileName(basePath, it.value()))) {
459             fprintf(stderr, "%s\n", qPrintable(helpGenerator.error()));
460             return -1;
461         }
462         ++it;
463     }
464 
465     fputs(qPrintable(QCG::tr("Creating collection file...\n")), stdout);
466 
467     QFileInfo colFi(collectionFile);
468     if (colFi.exists()) {
469         if (!colFi.dir().remove(colFi.fileName())) {
470             fputs(qPrintable(QCG::tr("The file %1 cannot be overwritten.\n")
471                              .arg(collectionFile)), stderr);
472             return -1;
473         }
474     }
475 
476     QHelpEngineCore helpEngine(collectionFile);
477     if (!helpEngine.setupData()) {
478         fprintf(stderr, "%s\n", qPrintable(helpEngine.error()));
479         return -1;
480     }
481 
482     foreach (const QString &file, config.filesToRegister()) {
483         if (!helpEngine.registerDocumentation(absoluteFileName(basePath, file))) {
484             fprintf(stderr, "%s\n", qPrintable(helpEngine.error()));
485             return -1;
486         }
487     }
488     if (!config.filesToRegister().isEmpty())
489         CollectionConfiguration::updateLastRegisterTime(helpEngine);
490 
491     if (!config.title().isEmpty())
492         CollectionConfiguration::setWindowTitle(helpEngine, config.title());
493 
494     if (!config.homePage().isEmpty()) {
495         CollectionConfiguration::setDefaultHomePage(helpEngine,
496             config.homePage());
497     }
498 
499     if (!config.startPage().isEmpty()) {
500         CollectionConfiguration::setLastShownPages(helpEngine,
501             QStringList(config.startPage()));
502     }
503 
504     if (!config.currentFilter().isEmpty()) {
505         helpEngine.setCurrentFilter(config.currentFilter());
506     }
507 
508     if (!config.cacheDirectory().isEmpty()) {
509         CollectionConfiguration::setCacheDir(helpEngine, config.cacheDirectory(),
510             config.cacheDirRelativeToCollection());
511     }
512 
513     CollectionConfiguration::setFilterFunctionalityEnabled(helpEngine,
514         config.enableFilterFunctionality());
515     CollectionConfiguration::setFilterToolbarVisible(helpEngine,
516         !config.hideFilterFunctionality());
517     CollectionConfiguration::setDocumentationManagerEnabled(helpEngine,
518         config.enableDocumentationManager());
519     CollectionConfiguration::setAddressBarEnabled(helpEngine,
520         config.enableAddressBar());
521     CollectionConfiguration::setAddressBarVisible(helpEngine,
522          !config.hideAddressBar());
523     CollectionConfiguration::setCreationTime(helpEngine,
524         QDateTime::currentDateTime().toTime_t());
525     CollectionConfiguration::setFullTextSearchFallbackEnabled(helpEngine,
526         config.fullTextSearchFallbackEnabled());
527 
528     if (!config.applicationIcon().isEmpty()) {
529         QFile icon(absoluteFileName(basePath, config.applicationIcon()));
530         if (!icon.open(QIODevice::ReadOnly)) {
531             fputs(qPrintable(QCG::tr("Cannot open %1.\n").arg(icon.fileName())), stderr);
532             return -1;
533         }
534         CollectionConfiguration::setApplicationIcon(helpEngine, icon.readAll());
535     }
536 
537     if (config.aboutMenuTexts().count()) {
538         QByteArray ba;
539         QDataStream s(&ba, QIODevice::WriteOnly);
540         QMap<QString, QString>::const_iterator it = config.aboutMenuTexts().constBegin();
541         while (it != config.aboutMenuTexts().constEnd()) {
542             s << it.key();
543             s << it.value();
544             ++it;
545         }
546         CollectionConfiguration::setAboutMenuTexts(helpEngine, ba);
547     }
548 
549     if (!config.aboutIcon().isEmpty()) {
550         QFile icon(absoluteFileName(basePath, config.aboutIcon()));
551         if (!icon.open(QIODevice::ReadOnly)) {
552             fputs(qPrintable(QCG::tr("Cannot open %1.\n").arg(icon.fileName())), stderr);
553             return -1;
554         }
555         CollectionConfiguration::setAboutIcon(helpEngine, icon.readAll());
556     }
557 
558     if (config.aboutTextFiles().count()) {
559         QByteArray ba;
560         QDataStream s(&ba, QIODevice::WriteOnly);
561         QMap<QString, QString>::const_iterator it = config.aboutTextFiles().constBegin();
562         QMap<QString, QByteArray> imgData;
563 
564         QRegExp srcRegExp(QLatin1String("src=(\"(.+)\"|([^\"\\s]+)).*>"));
565         srcRegExp.setMinimal(true);
566         QRegExp imgRegExp(QLatin1String("(<img[^>]+>)"));
567         imgRegExp.setMinimal(true);
568 
569         while (it != config.aboutTextFiles().constEnd()) {
570             s << it.key();
571             QFileInfo fi(absoluteFileName(basePath, it.value()));
572             QFile f(fi.absoluteFilePath());
573             if (!f.open(QIODevice::ReadOnly)) {
574                 fputs(qPrintable(QCG::tr("Cannot open %1.\n").arg(f.fileName())), stderr);
575                 return -1;
576             }
577             QByteArray data = f.readAll();
578             s << data;
579 
580             QString contents = QString::fromUtf8(data);
581             int pos = 0;
582             while ((pos = imgRegExp.indexIn(contents, pos)) != -1) {
583                 QString imgTag = imgRegExp.cap(1);
584                 pos += imgRegExp.matchedLength();
585 
586                 if (srcRegExp.indexIn(imgTag, 0) != -1) {
587                     QString src = srcRegExp.cap(2);
588                     if (src.isEmpty())
589                         src = srcRegExp.cap(3);
590 
591                     QFile img(fi.absolutePath() + QDir::separator() + src);
592                     if (img.open(QIODevice::ReadOnly)) {
593                         if (!imgData.contains(src))
594                             imgData.insert(src, img.readAll());
595                     } else {
596                         fputs(qPrintable(QCG::tr("Cannot open referenced image file %1.\n")
597                                          .arg(img.fileName())), stderr);
598                     }
599                 }
600             }
601             ++it;
602         }
603         CollectionConfiguration::setAboutTexts(helpEngine, ba);
604         if (imgData.count()) {
605             QByteArray imageData;
606             QBuffer buffer(&imageData);
607             buffer.open(QIODevice::WriteOnly);
608             QDataStream out(&buffer);
609             out << imgData;
610             CollectionConfiguration::setAboutImages(helpEngine, imageData);
611         }
612     }
613 
614     return 0;
615 }
616