1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3#include "manifestwriter.h"
4
5#include "config.h"
6#include "examplenode.h"
7#include "generator.h"
8#include "qdocdatabase.h"
9
10#include <QtCore/qmap.h>
11#include <QtCore/qset.h>
12#include <QtCore/qxmlstream.h>
13
14QT_BEGIN_NAMESPACE
15
16/*!
17 \internal
18
19 For each attribute in a map of attributes, checks if the attribute is
20 found in \a usedAttributes. If it is not found, issues a warning specific
21 to the attribute.
22 */
23void warnAboutUnusedAttributes(const QStringList &usedAttributes, const ExampleNode *example)
24{
25 QMap<QString, QString> attributesToWarnFor;
26 attributesToWarnFor.insert(QStringLiteral("imageUrl"),
27 QStringLiteral("Example documentation should have at least one '\\image'"));
28 attributesToWarnFor.insert(QStringLiteral("projectPath"),
29 QStringLiteral("Example has no project file"));
30
31 for (auto it = attributesToWarnFor.cbegin(); it != attributesToWarnFor.cend(); ++it) {
32 if (!usedAttributes.contains(str: it.key()))
33 example->doc().location().warning(message: example->name() + ": " + it.value());
34 }
35}
36
37/*!
38 \internal
39
40 Write the description element. The description for an example is set
41 with the \brief command. If no brief is available, the element is set
42 to "No description available".
43 */
44
45void writeDescription(QXmlStreamWriter *writer, const ExampleNode *example)
46{
47 Q_ASSERT(writer && example);
48 writer->writeStartElement(qualifiedName: "description");
49 const Text brief = example->doc().briefText();
50 if (!brief.isEmpty())
51 writer->writeCDATA(text: brief.toString());
52 else
53 writer->writeCDATA(text: QString("No description available"));
54 writer->writeEndElement(); // description
55}
56
57/*!
58 \internal
59
60 Returns a list of \a files that Qt Creator should open for the \a exampleName.
61 */
62QMap<int, QString> getFilesToOpen(const QStringList &files, const QString &exampleName)
63{
64 QMap<int, QString> filesToOpen;
65 for (const QString &file : files) {
66 QFileInfo fileInfo(file);
67 QString fileName = fileInfo.fileName().toLower();
68 // open .qml, .cpp and .h files with a
69 // basename matching the example (project) name
70 // QMap key indicates the priority -
71 // the lowest value will be the top-most file
72 if ((fileInfo.baseName().compare(s: exampleName, cs: Qt::CaseInsensitive) == 0)) {
73 if (fileName.endsWith(s: ".qml"))
74 filesToOpen.insert(key: 0, value: file);
75 else if (fileName.endsWith(s: ".cpp"))
76 filesToOpen.insert(key: 1, value: file);
77 else if (fileName.endsWith(s: ".h"))
78 filesToOpen.insert(key: 2, value: file);
79 }
80 // main.qml takes precedence over main.cpp
81 else if (fileName.endsWith(s: "main.qml")) {
82 filesToOpen.insert(key: 3, value: file);
83 } else if (fileName.endsWith(s: "main.cpp")) {
84 filesToOpen.insert(key: 4, value: file);
85 }
86 }
87
88 return filesToOpen;
89}
90
91/*!
92 \internal
93 \brief Writes the lists of files to open for the example.
94
95 Writes out the \a filesToOpen and the full \a installPath through \a writer.
96 */
97void writeFilesToOpen(QXmlStreamWriter &writer, const QString &installPath,
98 const QMap<int, QString> &filesToOpen)
99{
100 for (auto it = filesToOpen.constEnd(); it != filesToOpen.constBegin();) {
101 writer.writeStartElement(qualifiedName: "fileToOpen");
102 if (--it == filesToOpen.constBegin()) {
103 writer.writeAttribute(QStringLiteral("mainFile"), QStringLiteral("true"));
104 }
105 writer.writeCharacters(text: installPath + it.value());
106 writer.writeEndElement();
107 }
108}
109
110/*!
111 \internal
112 \brief Writes example metadata into \a writer.
113
114 For instance,
115
116
117 \ meta category {Application Example}
118
119 becomes
120
121 <meta>
122 <entry name="category">Application Example</entry>
123 <meta>
124*/
125static void writeMetaInformation(QXmlStreamWriter &writer, const QStringMultiMap &map)
126{
127 if (map.isEmpty())
128 return;
129
130 writer.writeStartElement(qualifiedName: "meta");
131 for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
132 writer.writeStartElement(qualifiedName: "entry");
133 writer.writeAttribute(QStringLiteral("name"), value: it.key());
134 writer.writeCharacters(text: it.value());
135 writer.writeEndElement(); // tag
136 }
137 writer.writeEndElement(); // meta
138}
139
140/*!
141 \class ManifestWriter
142 \internal
143 \brief The ManifestWriter is responsible for writing manifest files.
144 */
145ManifestWriter::ManifestWriter()
146{
147 Config &config = Config::instance();
148 m_project = config.get(CONFIG_PROJECT).asString();
149 m_outputDirectory = config.getOutputDir();
150 m_qdb = QDocDatabase::qdocDB();
151
152 const QString prefix = CONFIG_QHP + Config::dot + m_project + Config::dot;
153 m_manifestDir =
154 QLatin1String("qthelp://") + config.get(var: prefix + QLatin1String("namespace")).asString();
155 m_manifestDir +=
156 QLatin1Char('/') + config.get(var: prefix + QLatin1String("virtualFolder")).asString()
157 + QLatin1Char('/');
158 readManifestMetaContent();
159 m_examplesPath = config.get(CONFIG_EXAMPLESINSTALLPATH).asString();
160 if (!m_examplesPath.isEmpty())
161 m_examplesPath += QLatin1Char('/');
162}
163
164template <typename F>
165void ManifestWriter::processManifestMetaContent(const QString &fullName, F matchFunc)
166{
167 for (const auto &index : m_manifestMetaContent) {
168 const auto &names = index.m_names;
169 for (const QString &name : names) {
170 bool match;
171 qsizetype wildcard = name.indexOf(c: QChar('*'));
172 switch (wildcard) {
173 case -1: // no wildcard used, exact match required
174 match = (fullName == name);
175 break;
176 case 0: // '*' matches all examples
177 match = true;
178 break;
179 default: // match with wildcard at the end
180 match = fullName.startsWith(s: name.left(n: wildcard));
181 }
182 if (match)
183 matchFunc(index);
184 }
185 }
186}
187
188/*!
189 This function outputs one or more manifest files in XML.
190 They are used by Creator.
191 */
192void ManifestWriter::generateManifestFiles()
193{
194 generateExampleManifestFile();
195 m_qdb->exampleNodeMap().clear();
196 m_manifestMetaContent.clear();
197}
198
199/*
200 Returns Qt module name as lower case tag, stripping Qt prefix:
201 QtQuickControls -> quickcontrols
202 QtOpenGL -> opengl
203 QtQuick3D -> quick3d
204 */
205static QString moduleNameAsTag(const QString &module)
206{
207 QString moduleName = module;
208 if (moduleName.startsWith(s: "Qt"))
209 moduleName = moduleName.mid(position: 2);
210 // Some examples are in QtDoc module, but 'doc' as tag makes little sense
211 if (moduleName == "Doc")
212 return QString();
213 return moduleName.toLower();
214}
215
216/*
217 Return tags that were added with
218 \ meta {tag} {tag1[,tag2,...]}
219 or
220 \ meta {tags} {tag1[,tag2,...]}
221 from example metadata
222 */
223static QSet<QString> tagsAddedWithMetaCommand(const ExampleNode *example)
224{
225 Q_ASSERT(example);
226
227 QSet<QString> tags;
228 const QStringMultiMap *metaTagMap = example->doc().metaTagMap();
229 if (metaTagMap) {
230 QStringList originalTags = metaTagMap->values(key: "tag");
231 originalTags << metaTagMap->values(key: "tags");
232 for (const auto &tag : originalTags) {
233 const auto &tagList = tag.toLower().split(sep: QLatin1Char(','), behavior: Qt::SkipEmptyParts);
234 tags += QSet<QString>(tagList.constBegin(), tagList.constEnd());
235 }
236 }
237 return tags;
238}
239
240/*
241 Writes the contents of tags into writer, formatted as
242 <tags>tag1,tag2..</tags>
243 */
244static void writeTagsElement(QXmlStreamWriter *writer, const QSet<QString> &tags)
245{
246 Q_ASSERT(writer);
247 if (tags.isEmpty())
248 return;
249
250 writer->writeStartElement(qualifiedName: "tags");
251 QStringList sortedTags = tags.values();
252 sortedTags.sort();
253 writer->writeCharacters(text: sortedTags.join(sep: ","));
254 writer->writeEndElement(); // tags
255}
256
257/*!
258 This function is called by generateExampleManifestFiles(), once
259 for each manifest file to be generated.
260 */
261void ManifestWriter::generateExampleManifestFile()
262{
263 const ExampleNodeMap &exampleNodeMap = m_qdb->exampleNodeMap();
264 if (exampleNodeMap.isEmpty())
265 return;
266
267 const QString outputFileName = "examples-manifest.xml";
268 QFile outputFile(m_outputDirectory + QLatin1Char('/') + outputFileName);
269 if (!outputFile.open(flags: QFile::WriteOnly | QFile::Text))
270 return;
271
272 QXmlStreamWriter writer(&outputFile);
273 writer.setAutoFormatting(true);
274 writer.writeStartDocument();
275 writer.writeStartElement(qualifiedName: "instructionals");
276 writer.writeAttribute(qualifiedName: "module", value: m_project);
277 writer.writeStartElement(qualifiedName: "examples");
278
279 for (const auto &example : exampleNodeMap.values()) {
280 QMap<QString, QString> usedAttributes;
281 QSet<QString> tags;
282 const QString installPath = retrieveExampleInstallationPath(example);
283 const QString fullName = m_project + QLatin1Char('/') + example->title();
284
285 processManifestMetaContent(
286 fullName, matchFunc: [&](const ManifestMetaFilter &filter) { tags += filter.m_tags; });
287 tags += tagsAddedWithMetaCommand(example);
288 // omit from the manifest if explicitly marked broken
289 if (tags.contains(value: "broken"))
290 continue;
291
292 // attributes that are always written for the element
293 usedAttributes.insert(key: "name", value: example->title());
294 usedAttributes.insert(key: "docUrl", value: m_manifestDir + Generator::currentGenerator()->fileBase(node: example) + ".html");
295
296 if (!example->projectFile().isEmpty())
297 usedAttributes.insert(key: "projectPath", value: installPath + example->projectFile());
298 if (!example->imageFileName().isEmpty())
299 usedAttributes.insert(key: "imageUrl", value: m_manifestDir + example->imageFileName());
300
301 processManifestMetaContent(fullName, matchFunc: [&](const ManifestMetaFilter &filter) {
302 const auto attributes = filter.m_attributes;
303 for (const auto &attribute : attributes) {
304 const QLatin1Char div(':');
305 QStringList attrList = attribute.split(sep: div);
306 if (attrList.size() == 1)
307 attrList.append(QStringLiteral("true"));
308 QString attrName = attrList.takeFirst();
309 if (!usedAttributes.contains(key: attrName))
310 usedAttributes.insert(key: attrName, value: attrList.join(sep: div));
311 }
312 });
313
314 writer.writeStartElement(qualifiedName: "example");
315 for (auto it = usedAttributes.cbegin(); it != usedAttributes.cend(); ++it)
316 writer.writeAttribute(qualifiedName: it.key(), value: it.value());
317
318 warnAboutUnusedAttributes(usedAttributes: usedAttributes.keys(), example);
319 writeDescription(writer: &writer, example);
320
321 const QString moduleNameTag = moduleNameAsTag(module: m_project);
322 if (!moduleNameTag.isEmpty())
323 tags << moduleNameTag;
324 writeTagsElement(writer: &writer, tags);
325
326 const QString exampleName = example->name().mid(position: example->name().lastIndexOf(c: '/') + 1);
327 const auto files = example->files();
328 const QMap<int, QString> filesToOpen = getFilesToOpen(files, exampleName);
329 writeFilesToOpen(writer, installPath, filesToOpen);
330
331 if (const QStringMultiMap *metaTagMapP = example->doc().metaTagMap()) {
332 // Write \meta elements into the XML, except for 'tag', 'installpath',
333 // as they are handled separately
334 QStringMultiMap map = *metaTagMapP;
335 erase_if(map, pred: [](QStringMultiMap::iterator iter) {
336 return iter.key() == "tag" || iter.key() == "tags" || iter.key() == "installpath";
337 });
338 writeMetaInformation(writer, map);
339 }
340
341 writer.writeEndElement(); // example
342 }
343
344 writer.writeEndElement(); // examples
345
346 if (!m_exampleCategories.isEmpty()) {
347 writer.writeStartElement(qualifiedName: "categories");
348 for (const auto &examplecategory : m_exampleCategories) {
349 writer.writeStartElement(qualifiedName: "category");
350 writer.writeCharacters(text: examplecategory);
351 writer.writeEndElement();
352 }
353 writer.writeEndElement(); // categories
354 }
355
356 writer.writeEndElement(); // instructionals
357 writer.writeEndDocument();
358 outputFile.close();
359}
360
361/*!
362 Reads metacontent - additional attributes and tags to apply
363 when generating manifest files, read from config.
364
365 The manifest metacontent map is cleared immediately after
366 the manifest files have been generated.
367 */
368void ManifestWriter::readManifestMetaContent()
369{
370 Config &config = Config::instance();
371 const QStringList names{config.get(CONFIG_MANIFESTMETA
372 + Config::dot
373 + QStringLiteral("filters")).asStringList()};
374
375 for (const auto &manifest : names) {
376 ManifestMetaFilter filter;
377 QString prefix = CONFIG_MANIFESTMETA + Config::dot + manifest + Config::dot;
378 filter.m_names = config.get(var: prefix + QStringLiteral("names")).asStringSet();
379 filter.m_attributes = config.get(var: prefix + QStringLiteral("attributes")).asStringSet();
380 filter.m_tags = config.get(var: prefix + QStringLiteral("tags")).asStringSet();
381 m_manifestMetaContent.append(t: filter);
382 }
383
384 m_exampleCategories = config.get(CONFIG_MANIFESTMETA
385 + QStringLiteral(".examplecategories")).asStringList();
386}
387
388/*!
389 Retrieve the install path for the \a example as specified with
390 the \\meta command, or fall back to the one defined in .qdocconf.
391 */
392QString ManifestWriter::retrieveExampleInstallationPath(const ExampleNode *example) const
393{
394 QString installPath;
395 if (example->doc().metaTagMap())
396 installPath = example->doc().metaTagMap()->value(key: QLatin1String("installpath"));
397 if (installPath.isEmpty())
398 installPath = m_examplesPath;
399 if (!installPath.isEmpty() && !installPath.endsWith(c: QLatin1Char('/')))
400 installPath += QLatin1Char('/');
401
402 return installPath;
403}
404
405QT_END_NAMESPACE
406

source code of qttools/src/qdoc/qdoc/manifestwriter.cpp