diff options
Diffstat (limited to 'src/qdoc/qdoc/src/qdoc/manifestwriter.cpp')
-rw-r--r-- | src/qdoc/qdoc/src/qdoc/manifestwriter.cpp | 405 |
1 files changed, 405 insertions, 0 deletions
diff --git a/src/qdoc/qdoc/src/qdoc/manifestwriter.cpp b/src/qdoc/qdoc/src/qdoc/manifestwriter.cpp new file mode 100644 index 000000000..97bf7f190 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/manifestwriter.cpp @@ -0,0 +1,405 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +#include "manifestwriter.h" + +#include "config.h" +#include "examplenode.h" +#include "generator.h" +#include "qdocdatabase.h" + +#include <QtCore/qmap.h> +#include <QtCore/qset.h> +#include <QtCore/qxmlstream.h> + +QT_BEGIN_NAMESPACE + +/*! + \internal + + For each attribute in a map of attributes, checks if the attribute is + found in \a usedAttributes. If it is not found, issues a warning specific + to the attribute. + */ +void warnAboutUnusedAttributes(const QStringList &usedAttributes, const ExampleNode *example) +{ + QMap<QString, QString> attributesToWarnFor; + attributesToWarnFor.insert(QStringLiteral("imageUrl"), + QStringLiteral("Example documentation should have at least one '\\image'")); + attributesToWarnFor.insert(QStringLiteral("projectPath"), + QStringLiteral("Example has no project file")); + + for (auto it = attributesToWarnFor.cbegin(); it != attributesToWarnFor.cend(); ++it) { + if (!usedAttributes.contains(it.key())) + example->doc().location().warning(example->name() + ": " + it.value()); + } +} + +/*! + \internal + + Write the description element. The description for an example is set + with the \brief command. If no brief is available, the element is set + to "No description available". + */ + +void writeDescription(QXmlStreamWriter *writer, const ExampleNode *example) +{ + Q_ASSERT(writer && example); + writer->writeStartElement("description"); + const Text brief = example->doc().briefText(); + if (!brief.isEmpty()) + writer->writeCDATA(brief.toString()); + else + writer->writeCDATA(QString("No description available")); + writer->writeEndElement(); // description +} + +/*! + \internal + + Returns a list of \a files that Qt Creator should open for the \a exampleName. + */ +QMap<int, QString> getFilesToOpen(const QStringList &files, const QString &exampleName) +{ + QMap<int, QString> filesToOpen; + for (const QString &file : files) { + QFileInfo fileInfo(file); + QString fileName = fileInfo.fileName().toLower(); + // open .qml, .cpp and .h files with a + // basename matching the example (project) name + // QMap key indicates the priority - + // the lowest value will be the top-most file + if ((fileInfo.baseName().compare(exampleName, Qt::CaseInsensitive) == 0)) { + if (fileName.endsWith(".qml")) + filesToOpen.insert(0, file); + else if (fileName.endsWith(".cpp")) + filesToOpen.insert(1, file); + else if (fileName.endsWith(".h")) + filesToOpen.insert(2, file); + } + // main.qml takes precedence over main.cpp + else if (fileName.endsWith("main.qml")) { + filesToOpen.insert(3, file); + } else if (fileName.endsWith("main.cpp")) { + filesToOpen.insert(4, file); + } + } + + return filesToOpen; +} + +/*! + \internal + \brief Writes the lists of files to open for the example. + + Writes out the \a filesToOpen and the full \a installPath through \a writer. + */ +void writeFilesToOpen(QXmlStreamWriter &writer, const QString &installPath, + const QMap<int, QString> &filesToOpen) +{ + for (auto it = filesToOpen.constEnd(); it != filesToOpen.constBegin();) { + writer.writeStartElement("fileToOpen"); + if (--it == filesToOpen.constBegin()) { + writer.writeAttribute(QStringLiteral("mainFile"), QStringLiteral("true")); + } + writer.writeCharacters(installPath + it.value()); + writer.writeEndElement(); + } +} + +/*! + \internal + \brief Writes example metadata into \a writer. + + For instance, + + + \ meta category {Application Example} + + becomes + + <meta> + <entry name="category">Application Example</entry> + <meta> +*/ +static void writeMetaInformation(QXmlStreamWriter &writer, const QStringMultiMap &map) +{ + if (map.isEmpty()) + return; + + writer.writeStartElement("meta"); + for (auto it = map.constBegin(); it != map.constEnd(); ++it) { + writer.writeStartElement("entry"); + writer.writeAttribute(QStringLiteral("name"), it.key()); + writer.writeCharacters(it.value()); + writer.writeEndElement(); // tag + } + writer.writeEndElement(); // meta +} + +/*! + \class ManifestWriter + \internal + \brief The ManifestWriter is responsible for writing manifest files. + */ +ManifestWriter::ManifestWriter() +{ + Config &config = Config::instance(); + m_project = config.get(CONFIG_PROJECT).asString(); + m_outputDirectory = config.getOutputDir(); + m_qdb = QDocDatabase::qdocDB(); + + const QString prefix = CONFIG_QHP + Config::dot + m_project + Config::dot; + m_manifestDir = + QLatin1String("qthelp://") + config.get(prefix + QLatin1String("namespace")).asString(); + m_manifestDir += + QLatin1Char('/') + config.get(prefix + QLatin1String("virtualFolder")).asString() + + QLatin1Char('/'); + readManifestMetaContent(); + m_examplesPath = config.get(CONFIG_EXAMPLESINSTALLPATH).asString(); + if (!m_examplesPath.isEmpty()) + m_examplesPath += QLatin1Char('/'); +} + +template <typename F> +void ManifestWriter::processManifestMetaContent(const QString &fullName, F matchFunc) +{ + for (const auto &index : m_manifestMetaContent) { + const auto &names = index.m_names; + for (const QString &name : names) { + bool match; + qsizetype wildcard = name.indexOf(QChar('*')); + switch (wildcard) { + case -1: // no wildcard used, exact match required + match = (fullName == name); + break; + case 0: // '*' matches all examples + match = true; + break; + default: // match with wildcard at the end + match = fullName.startsWith(name.left(wildcard)); + } + if (match) + matchFunc(index); + } + } +} + +/*! + This function outputs one or more manifest files in XML. + They are used by Creator. + */ +void ManifestWriter::generateManifestFiles() +{ + generateExampleManifestFile(); + m_qdb->exampleNodeMap().clear(); + m_manifestMetaContent.clear(); +} + +/* + Returns Qt module name as lower case tag, stripping Qt prefix: + QtQuickControls -> quickcontrols + QtOpenGL -> opengl + QtQuick3D -> quick3d + */ +static QString moduleNameAsTag(const QString &module) +{ + QString moduleName = module; + if (moduleName.startsWith("Qt")) + moduleName = moduleName.mid(2); + // Some examples are in QtDoc module, but 'doc' as tag makes little sense + if (moduleName == "Doc") + return QString(); + return moduleName.toLower(); +} + +/* + Return tags that were added with + \ meta {tag} {tag1[,tag2,...]} + or + \ meta {tags} {tag1[,tag2,...]} + from example metadata + */ +static QSet<QString> tagsAddedWithMetaCommand(const ExampleNode *example) +{ + Q_ASSERT(example); + + QSet<QString> tags; + const QStringMultiMap *metaTagMap = example->doc().metaTagMap(); + if (metaTagMap) { + QStringList originalTags = metaTagMap->values("tag"); + originalTags << metaTagMap->values("tags"); + for (const auto &tag : originalTags) { + const auto &tagList = tag.toLower().split(QLatin1Char(','), Qt::SkipEmptyParts); + tags += QSet<QString>(tagList.constBegin(), tagList.constEnd()); + } + } + return tags; +} + +/* + Writes the contents of tags into writer, formatted as + <tags>tag1,tag2..</tags> + */ +static void writeTagsElement(QXmlStreamWriter *writer, const QSet<QString> &tags) +{ + Q_ASSERT(writer); + if (tags.isEmpty()) + return; + + writer->writeStartElement("tags"); + QStringList sortedTags = tags.values(); + sortedTags.sort(); + writer->writeCharacters(sortedTags.join(",")); + writer->writeEndElement(); // tags +} + +/*! + This function is called by generateExampleManifestFiles(), once + for each manifest file to be generated. + */ +void ManifestWriter::generateExampleManifestFile() +{ + const ExampleNodeMap &exampleNodeMap = m_qdb->exampleNodeMap(); + if (exampleNodeMap.isEmpty()) + return; + + const QString outputFileName = "examples-manifest.xml"; + QFile outputFile(m_outputDirectory + QLatin1Char('/') + outputFileName); + if (!outputFile.open(QFile::WriteOnly | QFile::Text)) + return; + + QXmlStreamWriter writer(&outputFile); + writer.setAutoFormatting(true); + writer.writeStartDocument(); + writer.writeStartElement("instructionals"); + writer.writeAttribute("module", m_project); + writer.writeStartElement("examples"); + + for (const auto &example : exampleNodeMap.values()) { + QMap<QString, QString> usedAttributes; + QSet<QString> tags; + const QString installPath = retrieveExampleInstallationPath(example); + const QString fullName = m_project + QLatin1Char('/') + example->title(); + + processManifestMetaContent( + fullName, [&](const ManifestMetaFilter &filter) { tags += filter.m_tags; }); + tags += tagsAddedWithMetaCommand(example); + // omit from the manifest if explicitly marked broken + if (tags.contains("broken")) + continue; + + // attributes that are always written for the element + usedAttributes.insert("name", example->title()); + usedAttributes.insert("docUrl", m_manifestDir + Generator::currentGenerator()->fileBase(example) + ".html"); + + if (!example->projectFile().isEmpty()) + usedAttributes.insert("projectPath", installPath + example->projectFile()); + if (!example->imageFileName().isEmpty()) + usedAttributes.insert("imageUrl", m_manifestDir + example->imageFileName()); + + processManifestMetaContent(fullName, [&](const ManifestMetaFilter &filter) { + const auto attributes = filter.m_attributes; + for (const auto &attribute : attributes) { + const QLatin1Char div(':'); + QStringList attrList = attribute.split(div); + if (attrList.size() == 1) + attrList.append(QStringLiteral("true")); + QString attrName = attrList.takeFirst(); + if (!usedAttributes.contains(attrName)) + usedAttributes.insert(attrName, attrList.join(div)); + } + }); + + writer.writeStartElement("example"); + for (auto it = usedAttributes.cbegin(); it != usedAttributes.cend(); ++it) + writer.writeAttribute(it.key(), it.value()); + + warnAboutUnusedAttributes(usedAttributes.keys(), example); + writeDescription(&writer, example); + + const QString moduleNameTag = moduleNameAsTag(m_project); + if (!moduleNameTag.isEmpty()) + tags << moduleNameTag; + writeTagsElement(&writer, tags); + + const QString exampleName = example->name().mid(example->name().lastIndexOf('/') + 1); + const auto files = example->files(); + const QMap<int, QString> filesToOpen = getFilesToOpen(files, exampleName); + writeFilesToOpen(writer, installPath, filesToOpen); + + if (const QStringMultiMap *metaTagMapP = example->doc().metaTagMap()) { + // Write \meta elements into the XML, except for 'tag', 'installpath', + // as they are handled separately + QStringMultiMap map = *metaTagMapP; + erase_if(map, [](QStringMultiMap::iterator iter) { + return iter.key() == "tag" || iter.key() == "tags" || iter.key() == "installpath"; + }); + writeMetaInformation(writer, map); + } + + writer.writeEndElement(); // example + } + + writer.writeEndElement(); // examples + + if (!m_exampleCategories.isEmpty()) { + writer.writeStartElement("categories"); + for (const auto &examplecategory : m_exampleCategories) { + writer.writeStartElement("category"); + writer.writeCharacters(examplecategory); + writer.writeEndElement(); + } + writer.writeEndElement(); // categories + } + + writer.writeEndElement(); // instructionals + writer.writeEndDocument(); + outputFile.close(); +} + +/*! + Reads metacontent - additional attributes and tags to apply + when generating manifest files, read from config. + + The manifest metacontent map is cleared immediately after + the manifest files have been generated. + */ +void ManifestWriter::readManifestMetaContent() +{ + Config &config = Config::instance(); + const QStringList names{config.get(CONFIG_MANIFESTMETA + + Config::dot + + QStringLiteral("filters")).asStringList()}; + + for (const auto &manifest : names) { + ManifestMetaFilter filter; + QString prefix = CONFIG_MANIFESTMETA + Config::dot + manifest + Config::dot; + filter.m_names = config.get(prefix + QStringLiteral("names")).asStringSet(); + filter.m_attributes = config.get(prefix + QStringLiteral("attributes")).asStringSet(); + filter.m_tags = config.get(prefix + QStringLiteral("tags")).asStringSet(); + m_manifestMetaContent.append(filter); + } + + m_exampleCategories = config.get(CONFIG_MANIFESTMETA + + QStringLiteral(".examplecategories")).asStringList(); +} + +/*! + Retrieve the install path for the \a example as specified with + the \\meta command, or fall back to the one defined in .qdocconf. + */ +QString ManifestWriter::retrieveExampleInstallationPath(const ExampleNode *example) const +{ + QString installPath; + if (example->doc().metaTagMap()) + installPath = example->doc().metaTagMap()->value(QLatin1String("installpath")); + if (installPath.isEmpty()) + installPath = m_examplesPath; + if (!installPath.isEmpty() && !installPath.endsWith(QLatin1Char('/'))) + installPath += QLatin1Char('/'); + + return installPath; +} + +QT_END_NAMESPACE |