1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qhelpprojectdata_p.h"
5
6#include <QtCore/QCoreApplication>
7#include <QtCore/QDir>
8#include <QtCore/QFileInfo>
9#include <QtCore/QStack>
10#include <QtCore/QMap>
11#include <QtCore/QRegularExpression>
12#include <QtCore/QTextStream>
13#include <QtCore/QUrl>
14#include <QtCore/QVariant>
15#include <QtCore/QXmlStreamReader>
16
17QT_BEGIN_NAMESPACE
18
19class QHelpProjectDataPrivate : public QXmlStreamReader
20{
21public:
22 void readData(const QByteArray &contents);
23
24 QString virtualFolder;
25 QString namespaceName;
26 QString fileName;
27 QString rootPath;
28
29 QList<QHelpDataCustomFilter> customFilterList;
30 QList<QHelpDataFilterSection> filterSectionList;
31 QMap<QString, QVariant> metaData;
32
33 QString errorMsg;
34
35private:
36 void readProject();
37 void readCustomFilter();
38 void readFilterSection();
39 void readTOC();
40 void readKeywords();
41 void readFiles();
42 void skipUnknownToken();
43 void addMatchingFiles(const QString &pattern);
44 bool hasValidSyntax(const QString &nameSpace, const QString &vFolder) const;
45
46 QMap<QString, QStringList> dirEntriesCache;
47};
48
49void QHelpProjectDataPrivate::skipUnknownToken()
50{
51 const QString message = QCoreApplication::translate(context: "QHelpProject",
52 key: "Skipping unknown token <%1> in file \"%2\".")
53 .arg(a: name()).arg(a: fileName) + QLatin1Char('\n');
54 fputs(qPrintable(message), stdout);
55
56 skipCurrentElement();
57}
58
59void QHelpProjectDataPrivate::readData(const QByteArray &contents)
60{
61 addData(data: contents);
62 while (!atEnd()) {
63 readNext();
64 if (isStartElement()) {
65 if (name() == QLatin1String("QtHelpProject")
66 && attributes().value(qualifiedName: QLatin1String("version"))
67 == QLatin1String("1.0")) {
68 readProject();
69 } else {
70 raiseError(message: QCoreApplication::translate(context: "QHelpProject",
71 key: "Unknown token. Expected \"QtHelpProject\"."));
72 }
73 }
74 }
75
76 if (hasError()) {
77 raiseError(message: QCoreApplication::translate(context: "QHelpProject",
78 key: "Error in line %1: %2").arg(a: lineNumber())
79 .arg(a: errorString()));
80 }
81}
82
83void QHelpProjectDataPrivate::readProject()
84{
85 while (!atEnd()) {
86 readNext();
87 if (isStartElement()) {
88 if (name() == QLatin1String("virtualFolder")) {
89 virtualFolder = readElementText();
90 if (!hasValidSyntax(nameSpace: QLatin1String("test"), vFolder: virtualFolder))
91 raiseError(message: QCoreApplication::translate(context: "QHelpProject",
92 key: "Virtual folder has invalid syntax in file: \"%1\"").arg(a: fileName));
93 } else if (name() == QLatin1String("namespace")) {
94 namespaceName = readElementText();
95 if (!hasValidSyntax(nameSpace: namespaceName, vFolder: QLatin1String("test")))
96 raiseError(message: QCoreApplication::translate(context: "QHelpProject",
97 key: "Namespace \"%1\" has invalid syntax in file: \"%2\"").arg(args&: namespaceName, args&: fileName));
98 } else if (name() == QLatin1String("customFilter")) {
99 readCustomFilter();
100 } else if (name() == QLatin1String("filterSection")) {
101 readFilterSection();
102 } else if (name() == QLatin1String("metaData")) {
103 QString n = attributes().value(qualifiedName: QLatin1String("name")).toString();
104 if (!metaData.contains(key: n))
105 metaData[n]
106 = attributes().value(qualifiedName: QLatin1String("value")).toString();
107 else
108 metaData.insert(key: n, value: attributes().
109 value(qualifiedName: QLatin1String("value")).toString());
110 } else {
111 skipUnknownToken();
112 }
113 } else if (isEndElement() && name() == QLatin1String("QtHelpProject")) {
114 if (namespaceName.isEmpty())
115 raiseError(message: QCoreApplication::translate(context: "QHelpProject",
116 key: "Missing namespace in QtHelpProject file: \"%1\"").arg(a: fileName));
117 else if (virtualFolder.isEmpty())
118 raiseError(message: QCoreApplication::translate(context: "QHelpProject",
119 key: "Missing virtual folder in QtHelpProject file: \"%1\"").arg(a: fileName));
120 break;
121 }
122 }
123}
124
125void QHelpProjectDataPrivate::readCustomFilter()
126{
127 QHelpDataCustomFilter filter;
128 filter.name = attributes().value(qualifiedName: QLatin1String("name")).toString();
129 while (!atEnd()) {
130 readNext();
131 if (isStartElement()) {
132 if (name() == QLatin1String("filterAttribute"))
133 filter.filterAttributes.append(t: readElementText());
134 else
135 skipUnknownToken();
136 } else if (isEndElement() && name() == QLatin1String("customFilter")) {
137 break;
138 }
139 }
140 customFilterList.append(t: filter);
141}
142
143void QHelpProjectDataPrivate::readFilterSection()
144{
145 filterSectionList.append(t: QHelpDataFilterSection());
146 while (!atEnd()) {
147 readNext();
148 if (isStartElement()) {
149 if (name() == QLatin1String("filterAttribute"))
150 filterSectionList.last().addFilterAttribute(filter: readElementText());
151 else if (name() == QLatin1String("toc"))
152 readTOC();
153 else if (name() == QLatin1String("keywords"))
154 readKeywords();
155 else if (name() == QLatin1String("files"))
156 readFiles();
157 else
158 skipUnknownToken();
159 } else if (isEndElement() && name() == QLatin1String("filterSection")) {
160 break;
161 }
162 }
163}
164
165void QHelpProjectDataPrivate::readTOC()
166{
167 QStack<QHelpDataContentItem*> contentStack;
168 QHelpDataContentItem *itm = nullptr;
169 while (!atEnd()) {
170 readNext();
171 if (isStartElement()) {
172 if (name() == QLatin1String("section")) {
173 const QString &title = attributes().value(qualifiedName: QLatin1String("title")).toString();
174 const QString &ref = attributes().value(qualifiedName: QLatin1String("ref")).toString();
175 if (contentStack.isEmpty()) {
176 itm = new QHelpDataContentItem(nullptr, title, ref);
177 filterSectionList.last().addContent(content: itm);
178 } else {
179 itm = new QHelpDataContentItem(contentStack.top(), title, ref);
180 }
181 contentStack.push(t: itm);
182 } else {
183 skipUnknownToken();
184 }
185 } else if (isEndElement()) {
186 if (name() == QLatin1String("section")) {
187 contentStack.pop();
188 continue;
189 } else if (name() == QLatin1String("toc") && contentStack.isEmpty()) {
190 return;
191 } else {
192 skipUnknownToken();
193 }
194 }
195 }
196}
197
198static inline QString msgMissingAttribute(const QString &fileName, qint64 lineNumber, const QString &name)
199{
200 QString result;
201 QTextStream str(&result);
202 str << QDir::toNativeSeparators(pathName: fileName) << ':' << lineNumber
203 << ": Missing attribute in <keyword";
204 if (!name.isEmpty())
205 str << " name=\"" << name << '"';
206 str << ">.";
207 return result;
208}
209
210void QHelpProjectDataPrivate::readKeywords()
211{
212 while (!atEnd()) {
213 readNext();
214 if (isStartElement()) {
215 if (name() == QLatin1String("keyword")) {
216 const QString &refAttribute = attributes().value(QStringLiteral("ref")).toString();
217 const QString &nameAttribute = attributes().value(QStringLiteral("name")).toString();
218 const QString &idAttribute = attributes().value(QStringLiteral("id")).toString();
219 if (refAttribute.isEmpty() || (nameAttribute.isEmpty() && idAttribute.isEmpty())) {
220 qWarning(msg: "%s", qPrintable(msgMissingAttribute(fileName, lineNumber(), nameAttribute)));
221 continue;
222 }
223 filterSectionList.last()
224 .addIndex(index: QHelpDataIndexItem(nameAttribute, idAttribute, refAttribute));
225 } else {
226 skipUnknownToken();
227 }
228 } else if (isEndElement()) {
229 if (name() == QLatin1String("keyword"))
230 continue;
231 else if (name() == QLatin1String("keywords"))
232 return;
233 else
234 skipUnknownToken();
235 }
236 }
237}
238
239void QHelpProjectDataPrivate::readFiles()
240{
241 while (!atEnd()) {
242 readNext();
243 if (isStartElement()) {
244 if (name() == QLatin1String("file"))
245 addMatchingFiles(pattern: readElementText());
246 else
247 skipUnknownToken();
248 } else if (isEndElement()) {
249 if (name() == QLatin1String("file"))
250 continue;
251 else if (name() == QLatin1String("files"))
252 return;
253 else
254 skipUnknownToken();
255 }
256 }
257}
258
259// Expand file pattern and add matches into list. If the pattern does not match
260// any files, insert the pattern itself so the QHelpGenerator will emit a
261// meaningful warning later.
262void QHelpProjectDataPrivate::addMatchingFiles(const QString &pattern)
263{
264 // The pattern matching is expensive, so we skip it if no
265 // wildcard symbols occur in the string.
266 if (!pattern.contains(c: QLatin1Char('?')) && !pattern.contains(c: QLatin1Char('*'))
267 && !pattern.contains(c: QLatin1Char('[')) && !pattern.contains(c: QLatin1Char(']'))) {
268 filterSectionList.last().addFile(file: pattern);
269 return;
270 }
271
272 const QFileInfo fileInfo(rootPath + QLatin1Char('/') + pattern);
273 const QDir &dir = fileInfo.dir();
274 const QString &path = dir.canonicalPath();
275
276 // QDir::entryList() is expensive, so we cache the results.
277 const auto &it = dirEntriesCache.constFind(key: path);
278 const QStringList &entries = it != dirEntriesCache.cend() ?
279 it.value() : dir.entryList(filters: QDir::Files);
280 if (it == dirEntriesCache.cend())
281 dirEntriesCache.insert(key: path, value: entries);
282
283 bool matchFound = false;
284#ifdef Q_OS_WIN
285 auto cs = QRegularExpression::CaseInsensitiveOption;
286#else
287 auto cs = QRegularExpression::NoPatternOption;
288#endif
289 const QRegularExpression regExp(QRegularExpression::wildcardToRegularExpression(str: fileInfo.fileName()), cs);
290 for (const QString &file : entries) {
291 auto match = regExp.match(subject: file);
292 if (match.hasMatch()) {
293 matchFound = true;
294 filterSectionList.last().
295 addFile(file: QFileInfo(pattern).dir().path() + QLatin1Char('/') + file);
296 }
297 }
298 if (!matchFound)
299 filterSectionList.last().addFile(file: pattern);
300}
301
302bool QHelpProjectDataPrivate::hasValidSyntax(const QString &nameSpace,
303 const QString &vFolder) const
304{
305 const QLatin1Char slash('/');
306 if (nameSpace.contains(c: slash) || vFolder.contains(c: slash))
307 return false;
308 QUrl url;
309 const QLatin1String scheme("qthelp");
310 url.setScheme(scheme);
311 const QString &canonicalNamespace = nameSpace.toLower();
312 url.setHost(host: canonicalNamespace);
313 url.setPath(path: slash + vFolder);
314
315 const QString expectedUrl(scheme + QLatin1String("://")
316 + canonicalNamespace + slash + vFolder);
317 return url.isValid() && url.toString() == expectedUrl;
318}
319
320/*!
321 \internal
322 \class QHelpProjectData
323 \since 4.4
324 \brief The QHelpProjectData class stores all information found
325 in a Qt help project file.
326
327 The structure is filled with data by calling readData(). The
328 specified file has to have the Qt help project file format in
329 order to be read successfully. Possible reading errors can be
330 retrieved by calling errorMessage().
331*/
332
333/*!
334 Constructs a Qt help project data structure.
335*/
336QHelpProjectData::QHelpProjectData()
337{
338 d = new QHelpProjectDataPrivate;
339}
340
341/*!
342 Destroys the help project data.
343*/
344QHelpProjectData::~QHelpProjectData()
345{
346 delete d;
347}
348
349/*!
350 Reads the file \a fileName and stores the help data. The file has to
351 have the Qt help project file format. Returns true if the file
352 was successfully read, otherwise false.
353
354 \sa errorMessage()
355*/
356bool QHelpProjectData::readData(const QString &fileName)
357{
358 d->fileName = fileName;
359 d->rootPath = QFileInfo(fileName).absolutePath();
360 QFile file(fileName);
361 if (!file.open(flags: QIODevice::ReadOnly)) {
362 d->errorMsg = QCoreApplication::translate(context: "QHelpProject",
363 key: "The input file %1 could not be opened.").arg(a: fileName);
364 return false;
365 }
366
367 d->readData(contents: file.readAll());
368 return !d->hasError();
369}
370
371/*!
372 Returns an error message if the reading of the Qt help project
373 file failed. Otherwise, an empty QString is returned.
374
375 \sa readData()
376*/
377QString QHelpProjectData::errorMessage() const
378{
379 if (d->hasError())
380 return d->errorString();
381 return d->errorMsg;
382}
383
384/*!
385 \internal
386*/
387QString QHelpProjectData::namespaceName() const
388{
389 return d->namespaceName;
390}
391
392/*!
393 \internal
394*/
395QString QHelpProjectData::virtualFolder() const
396{
397 return d->virtualFolder;
398}
399
400/*!
401 \internal
402*/
403QList<QHelpDataCustomFilter> QHelpProjectData::customFilters() const
404{
405 return d->customFilterList;
406}
407
408/*!
409 \internal
410*/
411QList<QHelpDataFilterSection> QHelpProjectData::filterSections() const
412{
413 return d->filterSectionList;
414}
415
416/*!
417 \internal
418*/
419QMap<QString, QVariant> QHelpProjectData::metaData() const
420{
421 return d->metaData;
422}
423
424/*!
425 \internal
426*/
427QString QHelpProjectData::rootPath() const
428{
429 return d->rootPath;
430}
431
432QT_END_NAMESPACE
433

source code of qttools/src/assistant/qhelpgenerator/qhelpprojectdata.cpp