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 | |