summaryrefslogtreecommitdiffstats
path: root/src/qdoc/qdoc/src/qdoc/helpprojectwriter.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/qdoc/qdoc/src/qdoc/helpprojectwriter.cpp')
-rw-r--r--src/qdoc/qdoc/src/qdoc/helpprojectwriter.cpp769
1 files changed, 769 insertions, 0 deletions
diff --git a/src/qdoc/qdoc/src/qdoc/helpprojectwriter.cpp b/src/qdoc/qdoc/src/qdoc/helpprojectwriter.cpp
new file mode 100644
index 000000000..968bb7b25
--- /dev/null
+++ b/src/qdoc/qdoc/src/qdoc/helpprojectwriter.cpp
@@ -0,0 +1,769 @@
+// 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 "helpprojectwriter.h"
+
+#include "access.h"
+#include "aggregate.h"
+#include "atom.h"
+#include "classnode.h"
+#include "collectionnode.h"
+#include "config.h"
+#include "enumnode.h"
+#include "functionnode.h"
+#include "htmlgenerator.h"
+#include "node.h"
+#include "qdocdatabase.h"
+#include "typedefnode.h"
+
+#include <QtCore/qhash.h>
+
+QT_BEGIN_NAMESPACE
+
+using namespace Qt::StringLiterals;
+
+HelpProjectWriter::HelpProjectWriter(const QString &defaultFileName, Generator *g)
+{
+ reset(defaultFileName, g);
+}
+
+void HelpProjectWriter::reset(const QString &defaultFileName, Generator *g)
+{
+ m_projects.clear();
+ m_gen = g;
+ /*
+ Get the pointer to the singleton for the qdoc database and
+ store it locally. This replaces all the local accesses to
+ the node tree, which are now private.
+ */
+ m_qdb = QDocDatabase::qdocDB();
+
+ // The output directory should already have been checked by the calling
+ // generator.
+ Config &config = Config::instance();
+ m_outputDir = config.getOutputDir();
+
+ const QStringList names{config.get(CONFIG_QHP + Config::dot + "projects").asStringList()};
+
+ for (const auto &projectName : names) {
+ HelpProject project;
+ project.m_name = projectName;
+
+ QString prefix = CONFIG_QHP + Config::dot + projectName + Config::dot;
+ project.m_helpNamespace = config.get(prefix + "namespace").asString();
+ project.m_virtualFolder = config.get(prefix + "virtualFolder").asString();
+ project.m_version = config.get(CONFIG_VERSION).asString();
+ project.m_fileName = config.get(prefix + "file").asString();
+ if (project.m_fileName.isEmpty())
+ project.m_fileName = defaultFileName;
+ project.m_extraFiles = config.get(prefix + "extraFiles").asStringSet();
+ project.m_extraFiles += config.get(CONFIG_QHP + Config::dot + "extraFiles").asStringSet();
+ project.m_indexTitle = config.get(prefix + "indexTitle").asString();
+ project.m_indexRoot = config.get(prefix + "indexRoot").asString();
+ project.m_filterAttributes = config.get(prefix + "filterAttributes").asStringSet();
+ project.m_includeIndexNodes = config.get(prefix + "includeIndexNodes").asBool();
+ const QSet<QString> customFilterNames = config.subVars(prefix + "customFilters");
+ for (const auto &filterName : customFilterNames) {
+ QString name{config.get(prefix + "customFilters" + Config::dot + filterName
+ + Config::dot + "name").asString()};
+ project.m_customFilters[name] =
+ config.get(prefix + "customFilters" + Config::dot + filterName
+ + Config::dot + "filterAttributes").asStringSet();
+ }
+
+ const auto excludedPrefixes = config.get(prefix + "excluded").asStringSet();
+ for (auto name : excludedPrefixes)
+ project.m_excluded.insert(name.replace(QLatin1Char('\\'), QLatin1Char('/')));
+
+ const auto subprojectPrefixes{config.get(prefix + "subprojects").asStringList()};
+ for (const auto &name : subprojectPrefixes) {
+ SubProject subproject;
+ QString subprefix = prefix + "subprojects" + Config::dot + name + Config::dot;
+ subproject.m_title = config.get(subprefix + "title").asString();
+ if (subproject.m_title.isEmpty())
+ continue;
+ subproject.m_indexTitle = config.get(subprefix + "indexTitle").asString();
+ subproject.m_sortPages = config.get(subprefix + "sortPages").asBool();
+ subproject.m_type = config.get(subprefix + "type").asString();
+ readSelectors(subproject, config.get(subprefix + "selectors").asStringList());
+ project.m_subprojects.append(subproject);
+ }
+
+ if (project.m_subprojects.isEmpty()) {
+ SubProject subproject;
+ readSelectors(subproject, config.get(prefix + "selectors").asStringList());
+ project.m_subprojects.insert(0, subproject);
+ }
+
+ m_projects.append(project);
+ }
+}
+
+void HelpProjectWriter::readSelectors(SubProject &subproject, const QStringList &selectors)
+{
+ QHash<QString, Node::NodeType> typeHash;
+ typeHash["namespace"] = Node::Namespace;
+ typeHash["class"] = Node::Class;
+ typeHash["struct"] = Node::Struct;
+ typeHash["union"] = Node::Union;
+ typeHash["header"] = Node::HeaderFile;
+ typeHash["headerfile"] = Node::HeaderFile;
+ typeHash["doc"] = Node::Page; // Unused (supported but ignored as a prefix)
+ typeHash["fake"] = Node::Page; // Unused (supported but ignored as a prefix)
+ typeHash["page"] = Node::Page;
+ typeHash["enum"] = Node::Enum;
+ typeHash["example"] = Node::Example;
+ typeHash["externalpage"] = Node::ExternalPage;
+ typeHash["typedef"] = Node::Typedef;
+ typeHash["typealias"] = Node::TypeAlias;
+ typeHash["function"] = Node::Function;
+ typeHash["property"] = Node::Property;
+ typeHash["variable"] = Node::Variable;
+ typeHash["group"] = Node::Group;
+ typeHash["module"] = Node::Module;
+ typeHash["qmlmodule"] = Node::QmlModule;
+ typeHash["qmlproperty"] = Node::QmlProperty;
+ typeHash["qmlclass"] = Node::QmlType; // Legacy alias for 'qmltype'
+ typeHash["qmltype"] = Node::QmlType;
+ typeHash["qmlbasictype"] = Node::QmlValueType; // Legacy alias for 'qmlvaluetype'
+ typeHash["qmlvaluetype"] = Node::QmlValueType;
+
+ for (const QString &selector : selectors) {
+ QStringList pieces = selector.split(QLatin1Char(':'));
+ // Remove doc: or fake: prefix
+ if (pieces.size() > 1 && typeHash.value(pieces[0].toLower()) == Node::Page)
+ pieces.takeFirst();
+
+ QString typeName = pieces.takeFirst().toLower();
+ if (!typeHash.contains(typeName))
+ continue;
+
+ subproject.m_selectors << typeHash.value(typeName);
+ if (!pieces.isEmpty()) {
+ pieces = pieces[0].split(QLatin1Char(','));
+ for (const auto &piece : std::as_const(pieces)) {
+ if (typeHash[typeName] == Node::Group
+ || typeHash[typeName] == Node::Module
+ || typeHash[typeName] == Node::QmlModule) {
+ subproject.m_groups << piece.toLower();
+ }
+ }
+ }
+ }
+}
+
+void HelpProjectWriter::addExtraFile(const QString &file)
+{
+ for (HelpProject &project : m_projects)
+ project.m_extraFiles.insert(file);
+}
+
+Keyword HelpProjectWriter::keywordDetails(const Node *node) const
+{
+ QString ref = m_gen->fullDocumentLocation(node);
+
+ if (node->parent() && !node->parent()->name().isEmpty()) {
+ QString name = (node->isEnumType() || node->isTypedef())
+ ? node->parent()->name()+"::"+node->name()
+ : node->name();
+ QString id = (!node->isRelatedNonmember())
+ ? node->parent()->name()+"::"+node->name()
+ : node->name();
+ return Keyword(name, id, ref);
+ } else if (node->isQmlType()) {
+ const QString &name = node->name();
+ QString moduleName = node->logicalModuleName();
+ QStringList ids("QML." + name);
+ if (!moduleName.isEmpty()) {
+ QString majorVersion = node->logicalModule()
+ ? node->logicalModule()->logicalModuleVersion().split('.')[0]
+ : QString();
+ ids << "QML." + moduleName + majorVersion + "." + name;
+ }
+ return Keyword(name, ids, ref);
+ } else if (node->isQmlModule()) {
+ const QLatin1Char delim('.');
+ QStringList parts = node->logicalModuleName().split(delim) << "QML";
+ std::reverse(parts.begin(), parts.end());
+ return Keyword(node->logicalModuleName(), parts.join(delim), ref);
+ } else if (node->isTextPageNode()) {
+ const auto *pageNode = static_cast<const PageNode *>(node);
+ return Keyword(pageNode->fullTitle(), pageNode->fullTitle(), ref);
+ } else {
+ return Keyword(node->name(), node->name(), ref);
+ }
+}
+
+bool HelpProjectWriter::generateSection(HelpProject &project, QXmlStreamWriter & /* writer */,
+ const Node *node)
+{
+ if (!node->url().isEmpty() && !(project.m_includeIndexNodes && !node->url().startsWith("http")))
+ return false;
+
+ if (node->isPrivate() || node->isInternal() || node->isDontDocument())
+ return false;
+
+ if (node->name().isEmpty())
+ return true;
+
+ QString docPath = node->doc().location().filePath();
+ if (!docPath.isEmpty() && project.m_excluded.contains(docPath))
+ return false;
+
+ QString objName = node->isTextPageNode() ? node->fullTitle() : node->fullDocumentName();
+ // Only add nodes to the set for each subproject if they match a selector.
+ // Those that match will be listed in the table of contents.
+
+ for (int i = 0; i < project.m_subprojects.size(); i++) {
+ SubProject subproject = project.m_subprojects[i];
+ // No selectors: accept all nodes.
+ if (subproject.m_selectors.isEmpty()) {
+ project.m_subprojects[i].m_nodes[objName] = node;
+ } else if (subproject.m_selectors.contains(node->nodeType())) {
+ // Add all group members for '[group|module|qmlmodule]:name' selector
+ if (node->isCollectionNode()) {
+ if (project.m_subprojects[i].m_groups.contains(node->name().toLower())) {
+ const auto *cn = static_cast<const CollectionNode *>(node);
+ const auto members = cn->members();
+ for (const Node *m : members) {
+ if (!m->isInAPI())
+ continue;
+ QString memberName =
+ m->isTextPageNode() ? m->fullTitle() : m->fullDocumentName();
+ project.m_subprojects[i].m_nodes[memberName] = m;
+ }
+ continue;
+ } else if (!project.m_subprojects[i].m_groups.isEmpty()) {
+ continue; // Node does not represent specified group(s)
+ }
+ } else if (node->isTextPageNode()) {
+ if (node->isExternalPage() || node->fullTitle().isEmpty())
+ continue;
+ }
+ project.m_subprojects[i].m_nodes[objName] = node;
+ }
+ }
+
+ switch (node->nodeType()) {
+
+ case Node::Class:
+ case Node::Struct:
+ case Node::Union:
+ project.m_keywords.append(keywordDetails(node));
+ break;
+ case Node::QmlType:
+ case Node::QmlValueType:
+ if (node->doc().hasKeywords()) {
+ const auto keywords = node->doc().keywords();
+ for (const Atom *keyword : keywords) {
+ if (!keyword->string().isEmpty()) {
+ project.m_keywords.append(Keyword(keyword->string(), keyword->string(),
+ m_gen->fullDocumentLocation(node)));
+ }
+ else
+ node->doc().location().warning(
+ QStringLiteral("Bad keyword in %1")
+ .arg(m_gen->fullDocumentLocation(node)));
+ }
+ }
+ project.m_keywords.append(keywordDetails(node));
+ break;
+
+ case Node::Namespace:
+ project.m_keywords.append(keywordDetails(node));
+ break;
+
+ case Node::Enum:
+ project.m_keywords.append(keywordDetails(node));
+ {
+ const auto *enumNode = static_cast<const EnumNode *>(node);
+ const auto items = enumNode->items();
+ for (const auto &item : items) {
+ if (enumNode->itemAccess(item.name()) == Access::Private)
+ continue;
+
+ QString name;
+ QString id;
+ if (!node->parent()->name().isEmpty()) {
+ name = id = node->parent()->name() + "::" + item.name();
+ } else {
+ name = id = item.name();
+ }
+ QString ref = m_gen->fullDocumentLocation(node);
+ project.m_keywords.append(Keyword(name, id, ref));
+ }
+ }
+ break;
+
+ case Node::Group:
+ case Node::Module:
+ case Node::QmlModule: {
+ const auto *cn = static_cast<const CollectionNode *>(node);
+ if (!cn->fullTitle().isEmpty()) {
+ if (cn->doc().hasKeywords()) {
+ const auto keywords = cn->doc().keywords();
+ for (const Atom *keyword : keywords) {
+ if (!keyword->string().isEmpty()) {
+ project.m_keywords.append(
+ Keyword(keyword->string(), keyword->string(),
+ m_gen->fullDocumentLocation(node)));
+ } else
+ cn->doc().location().warning(
+ QStringLiteral("Bad keyword in %1")
+ .arg(m_gen->fullDocumentLocation(node)));
+ }
+ }
+ project.m_keywords.append(keywordDetails(node));
+ }
+ } break;
+
+ case Node::Property:
+ case Node::QmlProperty:
+ project.m_keywords.append(keywordDetails(node));
+ break;
+
+ case Node::Function: {
+ const auto *funcNode = static_cast<const FunctionNode *>(node);
+
+ /*
+ QML methods, signals, and signal handlers used to be node types,
+ but now they are Function nodes with a Metaness value that specifies
+ what kind of function they are, QmlSignal, QmlMethod, etc.
+ */
+ if (funcNode->isQmlNode()) {
+ project.m_keywords.append(keywordDetails(node));
+ break;
+ }
+ // Only insert keywords for non-constructors. Constructors are covered
+ // by the classes themselves.
+
+ if (!funcNode->isSomeCtor())
+ project.m_keywords.append(keywordDetails(node));
+
+ // Insert member status flags into the entries for the parent
+ // node of the function, or the node it is related to.
+ // Since parent nodes should have already been inserted into
+ // the set of files, we only need to ensure that related nodes
+ // are inserted.
+
+ if (node->parent())
+ project.m_memberStatus[node->parent()].insert(node->status());
+ } break;
+ case Node::TypeAlias:
+ case Node::Typedef: {
+ const auto *typedefNode = static_cast<const TypedefNode *>(node);
+ Keyword typedefDetails = keywordDetails(node);
+ const EnumNode *enumNode = typedefNode->associatedEnum();
+ // Use the location of any associated enum node in preference
+ // to that of the typedef.
+ if (enumNode)
+ typedefDetails.m_ref = m_gen->fullDocumentLocation(enumNode);
+
+ project.m_keywords.append(typedefDetails);
+ } break;
+
+ case Node::Variable: {
+ project.m_keywords.append(keywordDetails(node));
+ } break;
+
+ // Page nodes (such as manual pages) contain subtypes, titles and other
+ // attributes.
+ case Node::Page: {
+ const auto *pn = static_cast<const PageNode *>(node);
+ if (!pn->fullTitle().isEmpty()) {
+ if (pn->doc().hasKeywords()) {
+ const auto keywords = pn->doc().keywords();
+ for (const Atom *keyword : keywords) {
+ if (!keyword->string().isEmpty()) {
+ project.m_keywords.append(
+ Keyword(keyword->string(), keyword->string(),
+ m_gen->fullDocumentLocation(node)));
+ } else {
+ QString loc = m_gen->fullDocumentLocation(node);
+ pn->doc().location().warning(QStringLiteral("Bad keyword in %1").arg(loc));
+ }
+ }
+ }
+ project.m_keywords.append(keywordDetails(node));
+ }
+ break;
+ }
+ default:;
+ }
+
+ // Add all images referenced in the page to the set of files to include.
+ const Atom *atom = node->doc().body().firstAtom();
+ while (atom) {
+ if (atom->type() == Atom::Image || atom->type() == Atom::InlineImage) {
+ // Images are all placed within a single directory regardless of
+ // whether the source images are in a nested directory structure.
+ QStringList pieces = atom->string().split(QLatin1Char('/'));
+ project.m_files.insert("images/" + pieces.last());
+ }
+ atom = atom->next();
+ }
+
+ return true;
+}
+
+void HelpProjectWriter::generateSections(HelpProject &project, QXmlStreamWriter &writer,
+ const Node *node)
+{
+ /*
+ Don't include index nodes in the help file.
+ */
+ if (node->isIndexNode())
+ return;
+ if (!generateSection(project, writer, node))
+ return;
+
+ if (node->isAggregate()) {
+ const auto *aggregate = static_cast<const Aggregate *>(node);
+
+ // Ensure that we don't visit nodes more than once.
+ NodeList childSet;
+ NodeList children = aggregate->childNodes();
+ std::sort(children.begin(), children.end(), Node::nodeNameLessThan);
+ for (auto *child : children) {
+ // Skip related non-members adopted by some other aggregate
+ if (child->parent() != aggregate)
+ continue;
+ if (child->isIndexNode() || child->isPrivate())
+ continue;
+ if (child->isTextPageNode()) {
+ if (!childSet.contains(child))
+ childSet << child;
+ } else {
+ // Store member status of children
+ project.m_memberStatus[node].insert(child->status());
+ if (child->isFunction() && static_cast<const FunctionNode *>(child)->isOverload())
+ continue;
+ if (!childSet.contains(child))
+ childSet << child;
+ }
+ }
+ for (const auto *child : std::as_const(childSet))
+ generateSections(project, writer, child);
+ }
+}
+
+void HelpProjectWriter::generate()
+{
+ // Warn if .qhp configuration was expected but not provided
+ if (auto &config = Config::instance(); m_projects.isEmpty() && config.get(CONFIG_QHP).asBool()) {
+ config.location().warning(u"Documentation configuration for '%1' doesn't define a help project (qhp)"_s
+ .arg(config.get(CONFIG_PROJECT).asString()));
+ }
+ for (HelpProject &project : m_projects)
+ generateProject(project);
+}
+
+void HelpProjectWriter::writeSection(QXmlStreamWriter &writer, const QString &path,
+ const QString &value)
+{
+ writer.writeStartElement(QStringLiteral("section"));
+ writer.writeAttribute(QStringLiteral("ref"), path);
+ writer.writeAttribute(QStringLiteral("title"), value);
+ writer.writeEndElement(); // section
+}
+
+/*!
+ Write subsections for all members, compatibility members and obsolete members.
+ */
+void HelpProjectWriter::addMembers(HelpProject &project, QXmlStreamWriter &writer, const Node *node)
+{
+ QString href = m_gen->fullDocumentLocation(node);
+ href = href.left(href.size() - 5);
+ if (href.isEmpty())
+ return;
+
+ bool derivedClass = false;
+ if (node->isClassNode())
+ derivedClass = !(static_cast<const ClassNode *>(node)->baseClasses().isEmpty());
+
+ // Do not generate a 'List of all members' for namespaces or header files,
+ // but always generate it for derived classes and QML types (but not QML value types)
+ if (!node->isNamespace() && !node->isHeader() && !node->isQmlBasicType()
+ && (derivedClass || node->isQmlType() || !project.m_memberStatus[node].isEmpty())) {
+ QString membersPath = href + QStringLiteral("-members.html");
+ writeSection(writer, membersPath, QStringLiteral("List of all members"));
+ }
+ if (project.m_memberStatus[node].contains(Node::Deprecated)) {
+ QString obsoletePath = href + QStringLiteral("-obsolete.html");
+ writeSection(writer, obsoletePath, QStringLiteral("Obsolete members"));
+ }
+}
+
+void HelpProjectWriter::writeNode(HelpProject &project, QXmlStreamWriter &writer, const Node *node)
+{
+ QString href = m_gen->fullDocumentLocation(node);
+ QString objName = node->name();
+
+ switch (node->nodeType()) {
+
+ case Node::Class:
+ case Node::Struct:
+ case Node::Union:
+ case Node::QmlType:
+ case Node::QmlValueType: {
+ QString typeStr = m_gen->typeString(node);
+ if (!typeStr.isEmpty())
+ typeStr[0] = typeStr[0].toTitleCase();
+ writer.writeStartElement("section");
+ writer.writeAttribute("ref", href);
+ if (node->parent() && !node->parent()->name().isEmpty())
+ writer.writeAttribute("title",
+ QStringLiteral("%1::%2 %3 Reference")
+ .arg(node->parent()->name(), objName, typeStr));
+ else
+ writer.writeAttribute("title", QStringLiteral("%1 %2 Reference").arg(objName, typeStr));
+
+ addMembers(project, writer, node);
+ writer.writeEndElement(); // section
+ } break;
+
+ case Node::Namespace:
+ writeSection(writer, href, objName);
+ break;
+
+ case Node::Example:
+ case Node::HeaderFile:
+ case Node::Page:
+ case Node::Group:
+ case Node::Module:
+ case Node::QmlModule: {
+ writer.writeStartElement("section");
+ writer.writeAttribute("ref", href);
+ writer.writeAttribute("title", node->fullTitle());
+ if (node->nodeType() == Node::HeaderFile)
+ addMembers(project, writer, node);
+ writer.writeEndElement(); // section
+ } break;
+ default:;
+ }
+}
+
+void HelpProjectWriter::generateProject(HelpProject &project)
+{
+ const Node *rootNode;
+
+ // Restrict searching only to the local (primary) tree
+ QList<Tree *> searchOrder = m_qdb->searchOrder();
+ m_qdb->setLocalSearch();
+
+ if (!project.m_indexRoot.isEmpty())
+ rootNode = m_qdb->findPageNodeByTitle(project.m_indexRoot);
+ else
+ rootNode = m_qdb->primaryTreeRoot();
+
+ if (rootNode == nullptr)
+ return;
+
+ project.m_files.clear();
+ project.m_keywords.clear();
+
+ QFile file(m_outputDir + QDir::separator() + project.m_fileName);
+ if (!file.open(QFile::WriteOnly))
+ return;
+
+ QXmlStreamWriter writer(&file);
+ writer.setAutoFormatting(true);
+ writer.writeStartDocument();
+ writer.writeStartElement("QtHelpProject");
+ writer.writeAttribute("version", "1.0");
+
+ // Write metaData, virtualFolder and namespace elements.
+ writer.writeTextElement("namespace", project.m_helpNamespace);
+ writer.writeTextElement("virtualFolder", project.m_virtualFolder);
+ writer.writeStartElement("metaData");
+ writer.writeAttribute("name", "version");
+ writer.writeAttribute("value", project.m_version);
+ writer.writeEndElement();
+
+ // Write customFilter elements.
+ for (auto it = project.m_customFilters.constBegin(); it != project.m_customFilters.constEnd();
+ ++it) {
+ writer.writeStartElement("customFilter");
+ writer.writeAttribute("name", it.key());
+ QStringList sortedAttributes = it.value().values();
+ sortedAttributes.sort();
+ for (const auto &filter : std::as_const(sortedAttributes))
+ writer.writeTextElement("filterAttribute", filter);
+ writer.writeEndElement(); // customFilter
+ }
+
+ // Start the filterSection.
+ writer.writeStartElement("filterSection");
+
+ // Write filterAttribute elements.
+ QStringList sortedFilterAttributes = project.m_filterAttributes.values();
+ sortedFilterAttributes.sort();
+ for (const auto &filterName : std::as_const(sortedFilterAttributes))
+ writer.writeTextElement("filterAttribute", filterName);
+
+ writer.writeStartElement("toc");
+ writer.writeStartElement("section");
+ const Node *node = m_qdb->findPageNodeByTitle(project.m_indexTitle);
+ if (!node)
+ node = m_qdb->findNodeByNameAndType(QStringList(project.m_indexTitle), &Node::isPageNode);
+ if (!node)
+ node = m_qdb->findNodeByNameAndType(QStringList("index.html"), &Node::isPageNode);
+ QString indexPath;
+ if (node)
+ indexPath = m_gen->fullDocumentLocation(node);
+ else
+ indexPath = "index.html";
+ writer.writeAttribute("ref", indexPath);
+ writer.writeAttribute("title", project.m_indexTitle);
+
+ generateSections(project, writer, rootNode);
+
+ for (int i = 0; i < project.m_subprojects.size(); i++) {
+ SubProject subproject = project.m_subprojects[i];
+
+ if (subproject.m_type == QLatin1String("manual")) {
+
+ const Node *indexPage = m_qdb->findNodeForTarget(subproject.m_indexTitle, nullptr);
+ if (indexPage) {
+ Text indexBody = indexPage->doc().body();
+ const Atom *atom = indexBody.firstAtom();
+ QStack<int> sectionStack;
+ bool inItem = false;
+
+ while (atom) {
+ switch (atom->type()) {
+ case Atom::ListLeft:
+ sectionStack.push(0);
+ break;
+ case Atom::ListRight:
+ if (sectionStack.pop() > 0)
+ writer.writeEndElement(); // section
+ break;
+ case Atom::ListItemLeft:
+ inItem = true;
+ break;
+ case Atom::ListItemRight:
+ inItem = false;
+ break;
+ case Atom::Link:
+ if (inItem) {
+ if (sectionStack.top() > 0)
+ writer.writeEndElement(); // section
+
+ const Node *page = m_qdb->findNodeForTarget(atom->string(), nullptr);
+ writer.writeStartElement("section");
+ QString indexPath = m_gen->fullDocumentLocation(page);
+ writer.writeAttribute("ref", indexPath);
+ writer.writeAttribute("title", atom->linkText());
+
+ sectionStack.top() += 1;
+ }
+ break;
+ default:;
+ }
+
+ if (atom == indexBody.lastAtom())
+ break;
+ atom = atom->next();
+ }
+ } else
+ rootNode->doc().location().warning(
+ QStringLiteral("Failed to find index: %1").arg(subproject.m_indexTitle));
+
+ } else {
+
+ writer.writeStartElement("section");
+ QString indexPath = m_gen->fullDocumentLocation(
+ m_qdb->findNodeForTarget(subproject.m_indexTitle, nullptr));
+ writer.writeAttribute("ref", indexPath);
+ writer.writeAttribute("title", subproject.m_title);
+
+ if (subproject.m_sortPages) {
+ QStringList titles = subproject.m_nodes.keys();
+ titles.sort();
+ for (const auto &title : std::as_const(titles)) {
+ writeNode(project, writer, subproject.m_nodes[title]);
+ }
+ } else {
+ // Find a contents node and navigate from there, using the NextLink values.
+ QSet<QString> visited;
+ bool contentsFound = false;
+ for (const auto *node : std::as_const(subproject.m_nodes)) {
+ QString nextTitle = node->links().value(Node::NextLink).first;
+ if (!nextTitle.isEmpty()
+ && node->links().value(Node::ContentsLink).first.isEmpty()) {
+
+ const Node *nextPage = m_qdb->findNodeForTarget(nextTitle, nullptr);
+
+ // Write the contents node.
+ writeNode(project, writer, node);
+ contentsFound = true;
+
+ while (nextPage) {
+ writeNode(project, writer, nextPage);
+ nextTitle = nextPage->links().value(Node::NextLink).first;
+ if (nextTitle.isEmpty() || visited.contains(nextTitle))
+ break;
+ nextPage = m_qdb->findNodeForTarget(nextTitle, nullptr);
+ visited.insert(nextTitle);
+ }
+ break;
+ }
+ }
+ // No contents/nextpage links found, write all nodes unsorted
+ if (!contentsFound) {
+ QList<const Node *> subnodes = subproject.m_nodes.values();
+
+ std::sort(subnodes.begin(), subnodes.end(), Node::nodeNameLessThan);
+
+ for (const auto *node : std::as_const(subnodes))
+ writeNode(project, writer, node);
+ }
+ }
+
+ writer.writeEndElement(); // section
+ }
+ }
+
+ // Restore original search order
+ m_qdb->setSearchOrder(searchOrder);
+
+ writer.writeEndElement(); // section
+ writer.writeEndElement(); // toc
+
+ writer.writeStartElement("keywords");
+ std::sort(project.m_keywords.begin(), project.m_keywords.end());
+ for (const auto &k : std::as_const(project.m_keywords)) {
+ for (const auto &id : std::as_const(k.m_ids)) {
+ writer.writeStartElement("keyword");
+ writer.writeAttribute("name", k.m_name);
+ writer.writeAttribute("id", id);
+ writer.writeAttribute("ref", k.m_ref);
+ writer.writeEndElement(); //keyword
+ }
+ }
+ writer.writeEndElement(); // keywords
+
+ writer.writeStartElement("files");
+
+ // The list of files to write is the union of generated files and
+ // other files (images and extras) included in the project
+ QSet<QString> files =
+ QSet<QString>(m_gen->outputFileNames().cbegin(), m_gen->outputFileNames().cend());
+ files.unite(project.m_files);
+ files.unite(project.m_extraFiles);
+ QStringList sortedFiles = files.values();
+ sortedFiles.sort();
+ for (const auto &usedFile : std::as_const(sortedFiles)) {
+ if (!usedFile.isEmpty())
+ writer.writeTextElement("file", usedFile);
+ }
+ writer.writeEndElement(); // files
+
+ writer.writeEndElement(); // filterSection
+ writer.writeEndElement(); // QtHelpProject
+ writer.writeEndDocument();
+ file.close();
+}
+
+QT_END_NAMESPACE