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