1 /****************************************************************************
2 **
3 ** Copyright (C) 2019 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the tools applications of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21 ** included in the packaging of this file. Please review the following
22 ** information to ensure the GNU General Public License requirements will
23 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24 **
25 ** $QT_END_LICENSE$
26 **
27 ****************************************************************************/
28 
29 #include "helpprojectwriter.h"
30 
31 #include "atom.h"
32 #include "config.h"
33 #include "htmlgenerator.h"
34 #include "node.h"
35 #include "qdocdatabase.h"
36 
37 #include <QtCore/qcryptographichash.h>
38 #include <QtCore/qdebug.h>
39 #include <QtCore/qhash.h>
40 #include <QtCore/qmap.h>
41 
42 QT_BEGIN_NAMESPACE
43 
HelpProjectWriter(const QString & defaultFileName,Generator * g)44 HelpProjectWriter::HelpProjectWriter(const QString &defaultFileName, Generator *g)
45 {
46     reset(defaultFileName, g);
47 }
48 
reset(const QString & defaultFileName,Generator * g)49 void HelpProjectWriter::reset(const QString &defaultFileName, Generator *g)
50 {
51     projects.clear();
52     gen_ = g;
53     /*
54       Get the pointer to the singleton for the qdoc database and
55       store it locally. This replaces all the local accesses to
56       the node tree, which are now private.
57      */
58     qdb_ = QDocDatabase::qdocDB();
59 
60     // The output directory should already have been checked by the calling
61     // generator.
62     Config &config = Config::instance();
63     outputDir = config.getOutputDir();
64 
65     const QStringList names = config.getStringList(CONFIG_QHP + Config::dot + "projects");
66 
67     for (const auto &projectName : names) {
68         HelpProject project;
69         project.name = projectName;
70 
71         QString prefix = CONFIG_QHP + Config::dot + projectName + Config::dot;
72         project.helpNamespace = config.getString(prefix + "namespace");
73         project.virtualFolder = config.getString(prefix + "virtualFolder");
74         project.version = config.getString(CONFIG_VERSION);
75         project.fileName = config.getString(prefix + "file");
76         if (project.fileName.isEmpty())
77             project.fileName = defaultFileName;
78         project.extraFiles = config.getStringSet(prefix + "extraFiles");
79         project.extraFiles += config.getStringSet(CONFIG_QHP + Config::dot + "extraFiles");
80         project.indexTitle = config.getString(prefix + "indexTitle");
81         project.indexRoot = config.getString(prefix + "indexRoot");
82         const auto &filterAttributes = config.getStringList(prefix + "filterAttributes");
83         project.filterAttributes =
84                 QSet<QString>(filterAttributes.cbegin(), filterAttributes.cend());
85         project.includeIndexNodes = config.getBool(prefix + "includeIndexNodes");
86         const QSet<QString> customFilterNames = config.subVars(prefix + "customFilters");
87         for (const auto &filterName : customFilterNames) {
88             QString name = config.getString(prefix + "customFilters" + Config::dot + filterName
89                                             + Config::dot + "name");
90             const auto &filters =
91                     config.getStringList(prefix + "customFilters" + Config::dot + filterName
92                                          + Config::dot + "filterAttributes");
93             project.customFilters[name] = QSet<QString>(filters.cbegin(), filters.cend());
94         }
95 
96         const auto excludedPrefixes = config.getStringSet(prefix + "excluded");
97         for (auto name : excludedPrefixes)
98             project.excluded.insert(name.replace(QLatin1Char('\\'), QLatin1Char('/')));
99 
100         const auto subprojectPrefixes = config.getStringList(prefix + "subprojects");
101         for (const auto &name : subprojectPrefixes) {
102             SubProject subproject;
103             QString subprefix = prefix + "subprojects" + Config::dot + name + Config::dot;
104             subproject.title = config.getString(subprefix + "title");
105             if (subproject.title.isEmpty())
106                 continue;
107             subproject.indexTitle = config.getString(subprefix + "indexTitle");
108             subproject.sortPages = config.getBool(subprefix + "sortPages");
109             subproject.type = config.getString(subprefix + "type");
110             readSelectors(subproject, config.getStringList(subprefix + "selectors"));
111             project.subprojects.append(subproject);
112         }
113 
114         if (project.subprojects.isEmpty()) {
115             SubProject subproject;
116             readSelectors(subproject, config.getStringList(prefix + "selectors"));
117             project.subprojects.insert(0, subproject);
118         }
119 
120         projects.append(project);
121     }
122 }
123 
readSelectors(SubProject & subproject,const QStringList & selectors)124 void HelpProjectWriter::readSelectors(SubProject &subproject, const QStringList &selectors)
125 {
126     QHash<QString, Node::NodeType> typeHash;
127     typeHash["namespace"] = Node::Namespace;
128     typeHash["class"] = Node::Class;
129     typeHash["struct"] = Node::Struct;
130     typeHash["union"] = Node::Union;
131     typeHash["header"] = Node::HeaderFile;
132     typeHash["headerfile"] = Node::HeaderFile;
133     typeHash["doc"] = Node::Page; // Unused (supported but ignored as a prefix)
134     typeHash["fake"] = Node::Page; // Unused (supported but ignored as a prefix)
135     typeHash["page"] = Node::Page;
136     typeHash["enum"] = Node::Enum;
137     typeHash["example"] = Node::Example;
138     typeHash["externalpage"] = Node::ExternalPage;
139     typeHash["typedef"] = Node::Typedef;
140     typeHash["typealias"] = Node::TypeAlias;
141     typeHash["function"] = Node::Function;
142     typeHash["property"] = Node::Property;
143     typeHash["variable"] = Node::Variable;
144     typeHash["group"] = Node::Group;
145     typeHash["module"] = Node::Module;
146     typeHash["jsmodule"] = Node::JsModule;
147     typeHash["qmlmodule"] = Node::QmlModule;
148     typeHash["qmlproperty"] = Node::JsProperty;
149     typeHash["jsproperty"] = Node::QmlProperty;
150     typeHash["qmlclass"] = Node::QmlType; // Legacy alias for 'qmltype'
151     typeHash["qmltype"] = Node::QmlType;
152     typeHash["qmlbasictype"] = Node::QmlBasicType;
153 
154     for (const QString &selector : selectors) {
155         QStringList pieces = selector.split(QLatin1Char(':'));
156         // Remove doc: or fake: prefix
157         if (pieces.size() > 1 && typeHash.value(pieces[0].toLower()) == Node::Page)
158             pieces.takeFirst();
159 
160         QString typeName = pieces.takeFirst().toLower();
161         if (!typeHash.contains(typeName))
162             continue;
163 
164         subproject.selectors << typeHash.value(typeName);
165         if (!pieces.isEmpty()) {
166             pieces = pieces[0].split(QLatin1Char(','));
167             for (const auto &piece : qAsConst(pieces)) {
168                 if (typeHash[typeName] == Node::Group
169                     || typeHash[typeName] == Node::Module
170                     || typeHash[typeName] == Node::QmlModule
171                     || typeHash[typeName] == Node::JsModule) {
172                     subproject.groups << piece.toLower();
173                 }
174             }
175         }
176     }
177 }
178 
addExtraFile(const QString & file)179 void HelpProjectWriter::addExtraFile(const QString &file)
180 {
181     for (int i = 0; i < projects.size(); ++i)
182         projects[i].extraFiles.insert(file);
183 }
184 
addExtraFiles(const QSet<QString> & files)185 void HelpProjectWriter::addExtraFiles(const QSet<QString> &files)
186 {
187     for (int i = 0; i < projects.size(); ++i)
188         projects[i].extraFiles.unite(files);
189 }
190 
191 /*!
192     Returns a list of strings describing the keyword details for a given node.
193 
194     The first string is the human-readable name to be shown in Assistant.
195     The second string is a unique identifier.
196     The third string is the location of the documentation for the keyword.
197  */
keywordDetails(const Node * node) const198 QStringList HelpProjectWriter::keywordDetails(const Node *node) const
199 {
200     QStringList details;
201 
202     if (node->parent() && !node->parent()->name().isEmpty()) {
203         // "name"
204         if (node->isEnumType() || node->isTypedef())
205             details << node->parent()->name() + "::" + node->name();
206         else
207             details << node->name();
208         // "id"
209         if (!node->isRelatedNonmember())
210             details << node->parent()->name() + "::" + node->name();
211         else
212             details << node->name();
213     } else if (node->isQmlType() || node->isQmlBasicType()) {
214         details << node->name();
215         details << "QML." + node->name();
216     } else if (node->isJsType() || node->isJsBasicType()) {
217         details << node->name();
218         details << "JS." + node->name();
219     } else if (node->isTextPageNode()) {
220         const PageNode *fake = static_cast<const PageNode *>(node);
221         details << fake->fullTitle();
222         details << fake->fullTitle();
223     } else {
224         details << node->name();
225         details << node->name();
226     }
227     details << gen_->fullDocumentLocation(node, false);
228     return details;
229 }
230 
generateSection(HelpProject & project,QXmlStreamWriter &,const Node * node)231 bool HelpProjectWriter::generateSection(HelpProject &project, QXmlStreamWriter & /* writer */,
232                                         const Node *node)
233 {
234     if (!node->url().isEmpty() && !(project.includeIndexNodes && !node->url().startsWith("http")))
235         return false;
236 
237     if (node->isPrivate() || node->isInternal() || node->isDontDocument())
238         return false;
239 
240     if (node->name().isEmpty())
241         return true;
242 
243     QString docPath = node->doc().location().filePath();
244     if (!docPath.isEmpty() && project.excluded.contains(docPath))
245         return false;
246 
247     QString objName = node->isTextPageNode() ? node->fullTitle() : node->fullDocumentName();
248     // Only add nodes to the set for each subproject if they match a selector.
249     // Those that match will be listed in the table of contents.
250 
251     for (int i = 0; i < project.subprojects.length(); i++) {
252         SubProject subproject = project.subprojects[i];
253         // No selectors: accept all nodes.
254         if (subproject.selectors.isEmpty()) {
255             project.subprojects[i].nodes[objName] = node;
256         } else if (subproject.selectors.contains(node->nodeType())) {
257             // Add all group members for '[group|module|qmlmodule]:name' selector
258             if (node->isCollectionNode()) {
259                 if (project.subprojects[i].groups.contains(node->name().toLower())) {
260                     const CollectionNode *cn = static_cast<const CollectionNode *>(node);
261                     const auto members = cn->members();
262                     for (const Node *m : members) {
263                         QString memberName =
264                                 m->isTextPageNode() ? m->fullTitle() : m->fullDocumentName();
265                         project.subprojects[i].nodes[memberName] = m;
266                     }
267                     continue;
268                 } else if (!project.subprojects[i].groups.isEmpty()) {
269                     continue; // Node does not represent specified group(s)
270                 }
271             } else if (node->isTextPageNode()) {
272                 if (node->isExternalPage() || node->fullTitle().isEmpty())
273                     continue;
274             }
275             project.subprojects[i].nodes[objName] = node;
276         }
277     }
278 
279     switch (node->nodeType()) {
280 
281     case Node::Class:
282     case Node::Struct:
283     case Node::Union:
284         project.keywords.append(keywordDetails(node));
285         break;
286     case Node::QmlType:
287     case Node::QmlBasicType:
288     case Node::JsType:
289     case Node::JsBasicType:
290         if (node->doc().hasKeywords()) {
291             const auto keywords = node->doc().keywords();
292             for (const Atom *keyword : keywords) {
293                 if (!keyword->string().isEmpty()) {
294                     QStringList details;
295                     details << keyword->string() << keyword->string()
296                             << gen_->fullDocumentLocation(node, false);
297                     project.keywords.append(details);
298                 } else
299                     node->doc().location().warning(
300                             tr("Bad keyword in %1").arg(gen_->fullDocumentLocation(node, false)));
301             }
302         }
303         project.keywords.append(keywordDetails(node));
304         break;
305 
306     case Node::Namespace:
307         project.keywords.append(keywordDetails(node));
308         break;
309 
310     case Node::Enum:
311         project.keywords.append(keywordDetails(node));
312         {
313             const EnumNode *enumNode = static_cast<const EnumNode *>(node);
314             const auto items = enumNode->items();
315             for (const auto &item : items) {
316                 QStringList details;
317 
318                 if (enumNode->itemAccess(item.name()) == Node::Private)
319                     continue;
320 
321                 if (!node->parent()->name().isEmpty()) {
322                     details << node->parent()->name() + "::" + item.name(); // "name"
323                     details << node->parent()->name() + "::" + item.name(); // "id"
324                 } else {
325                     details << item.name(); // "name"
326                     details << item.name(); // "id"
327                 }
328                 details << gen_->fullDocumentLocation(node, false);
329                 project.keywords.append(details);
330             }
331         }
332         break;
333 
334     case Node::Group:
335     case Node::Module:
336     case Node::QmlModule:
337     case Node::JsModule: {
338         const CollectionNode *cn = static_cast<const CollectionNode *>(node);
339         if (!cn->fullTitle().isEmpty()) {
340             if (cn->doc().hasKeywords()) {
341                 const auto keywords = cn->doc().keywords();
342                 for (const Atom *keyword : keywords) {
343                     if (!keyword->string().isEmpty()) {
344                         QStringList details;
345                         details << keyword->string() << keyword->string()
346                                 << gen_->fullDocumentLocation(node, false);
347                         project.keywords.append(details);
348                     } else
349                         cn->doc().location().warning(
350                                 tr("Bad keyword in %1")
351                                         .arg(gen_->fullDocumentLocation(node, false)));
352                 }
353             }
354             project.keywords.append(keywordDetails(node));
355         }
356     } break;
357 
358     case Node::Property:
359     case Node::QmlProperty:
360     case Node::JsProperty:
361         project.keywords.append(keywordDetails(node));
362         break;
363 
364     case Node::Function: {
365         const FunctionNode *funcNode = static_cast<const FunctionNode *>(node);
366 
367         /*
368           QML and JS methods, signals, and signal handlers used to be node types,
369           but now they are Function nodes with a Metaness value that specifies
370           what kind of function they are, QmlSignal, JsSignal, QmlMethod, etc. It
371           suffices at this point to test whether the node is of the QML or JS Genus,
372           because we already know it is NodeType::Function.
373          */
374         if (funcNode->isQmlNode() || funcNode->isJsNode()) {
375             project.keywords.append(keywordDetails(node));
376             break;
377         }
378         // Only insert keywords for non-constructors. Constructors are covered
379         // by the classes themselves.
380 
381         if (!funcNode->isSomeCtor())
382             project.keywords.append(keywordDetails(node));
383 
384         // Insert member status flags into the entries for the parent
385         // node of the function, or the node it is related to.
386         // Since parent nodes should have already been inserted into
387         // the set of files, we only need to ensure that related nodes
388         // are inserted.
389 
390         if (node->parent())
391             project.memberStatus[node->parent()].insert(node->status());
392     } break;
393     case Node::TypeAlias:
394     case Node::Typedef: {
395         const TypedefNode *typedefNode = static_cast<const TypedefNode *>(node);
396         QStringList typedefDetails = keywordDetails(node);
397         const EnumNode *enumNode = typedefNode->associatedEnum();
398         // Use the location of any associated enum node in preference
399         // to that of the typedef.
400         if (enumNode)
401             typedefDetails[2] = gen_->fullDocumentLocation(enumNode, false);
402 
403         project.keywords.append(typedefDetails);
404     } break;
405 
406     case Node::Variable: {
407         project.keywords.append(keywordDetails(node));
408     } break;
409 
410         // Page nodes (such as manual pages) contain subtypes, titles and other
411         // attributes.
412     case Node::Page: {
413         const PageNode *pn = static_cast<const PageNode *>(node);
414         if (!pn->fullTitle().isEmpty()) {
415             if (pn->doc().hasKeywords()) {
416                 const auto keywords = pn->doc().keywords();
417                 for (const Atom *keyword : keywords) {
418                     if (!keyword->string().isEmpty()) {
419                         QStringList details;
420                         details << keyword->string() << keyword->string()
421                                 << gen_->fullDocumentLocation(node, false);
422                         project.keywords.append(details);
423                     } else {
424                         QString loc = gen_->fullDocumentLocation(node, false);
425                         pn->doc().location().warning(tr("Bad keyword in %1").arg(loc));
426                     }
427                 }
428             }
429             project.keywords.append(keywordDetails(node));
430         }
431         break;
432     }
433     default:;
434     }
435 
436     // Add all images referenced in the page to the set of files to include.
437     const Atom *atom = node->doc().body().firstAtom();
438     while (atom) {
439         if (atom->type() == Atom::Image || atom->type() == Atom::InlineImage) {
440             // Images are all placed within a single directory regardless of
441             // whether the source images are in a nested directory structure.
442             QStringList pieces = atom->string().split(QLatin1Char('/'));
443             project.files.insert("images/" + pieces.last());
444         }
445         atom = atom->next();
446     }
447 
448     return true;
449 }
450 
generateSections(HelpProject & project,QXmlStreamWriter & writer,const Node * node)451 void HelpProjectWriter::generateSections(HelpProject &project, QXmlStreamWriter &writer,
452                                          const Node *node)
453 {
454     /*
455       Don't include index nodes in the help file.
456      */
457     if (node->isIndexNode())
458         return;
459     if (!generateSection(project, writer, node))
460         return;
461 
462     if (node->isAggregate()) {
463         const Aggregate *aggregate = static_cast<const Aggregate *>(node);
464 
465         // Ensure that we don't visit nodes more than once.
466         QSet<const Node *> childSet;
467         const NodeList &children = aggregate->childNodes();
468         for (const auto *child : children) {
469             // Skip related non-members adopted by some other aggregate
470             if (child->parent() != aggregate)
471                 continue;
472             if (child->isIndexNode() || child->isPrivate())
473                 continue;
474             if (child->isTextPageNode()) {
475                 childSet << child;
476             } else {
477                 // Store member status of children
478                 project.memberStatus[node].insert(child->status());
479                 if (child->isFunction() && static_cast<const FunctionNode *>(child)->isOverload())
480                     continue;
481                 childSet << child;
482             }
483         }
484         for (const auto *child : qAsConst(childSet))
485             generateSections(project, writer, child);
486     }
487 }
488 
generate()489 void HelpProjectWriter::generate()
490 {
491     for (int i = 0; i < projects.size(); ++i)
492         generateProject(projects[i]);
493 }
494 
writeHashFile(QFile & file)495 void HelpProjectWriter::writeHashFile(QFile &file)
496 {
497     QCryptographicHash hash(QCryptographicHash::Sha1);
498     hash.addData(&file);
499 
500     QFile hashFile(file.fileName() + ".sha1");
501     if (!hashFile.open(QFile::WriteOnly | QFile::Text))
502         return;
503 
504     hashFile.write(hash.result().toHex());
505     hashFile.close();
506 }
507 
writeSection(QXmlStreamWriter & writer,const QString & path,const QString & value)508 void HelpProjectWriter::writeSection(QXmlStreamWriter &writer, const QString &path,
509                                      const QString &value)
510 {
511     writer.writeStartElement(QStringLiteral("section"));
512     writer.writeAttribute(QStringLiteral("ref"), path);
513     writer.writeAttribute(QStringLiteral("title"), value);
514     writer.writeEndElement(); // section
515 }
516 
517 /*!
518     Write subsections for all members, compatibility members and obsolete members.
519  */
addMembers(HelpProject & project,QXmlStreamWriter & writer,const Node * node)520 void HelpProjectWriter::addMembers(HelpProject &project, QXmlStreamWriter &writer, const Node *node)
521 {
522     if (node->isQmlBasicType() || node->isJsBasicType())
523         return;
524 
525     QString href = gen_->fullDocumentLocation(node, false);
526     href = href.left(href.size() - 5);
527     if (href.isEmpty())
528         return;
529 
530     bool derivedClass = false;
531     if (node->isClassNode())
532         derivedClass = !(static_cast<const ClassNode *>(node)->baseClasses().isEmpty());
533 
534     // Do not generate a 'List of all members' for namespaces or header files,
535     // but always generate it for derived classes and QML classes
536     if (!node->isNamespace() && !node->isHeader()
537         && (derivedClass || node->isQmlType() || node->isJsType()
538             || !project.memberStatus[node].isEmpty())) {
539         QString membersPath = href + QStringLiteral("-members.html");
540         writeSection(writer, membersPath, tr("List of all members"));
541     }
542     if (project.memberStatus[node].contains(Node::Obsolete)) {
543         QString obsoletePath = href + QStringLiteral("-obsolete.html");
544         writeSection(writer, obsoletePath, tr("Obsolete members"));
545     }
546 }
547 
writeNode(HelpProject & project,QXmlStreamWriter & writer,const Node * node)548 void HelpProjectWriter::writeNode(HelpProject &project, QXmlStreamWriter &writer, const Node *node)
549 {
550     QString href = gen_->fullDocumentLocation(node, false);
551     QString objName = node->name();
552 
553     switch (node->nodeType()) {
554 
555     case Node::Class:
556     case Node::Struct:
557     case Node::Union:
558     case Node::QmlType:
559     case Node::JsType:
560     case Node::QmlBasicType:
561     case Node::JsBasicType: {
562         QString typeStr = gen_->typeString(node);
563         if (!typeStr.isEmpty())
564             typeStr[0] = typeStr[0].toTitleCase();
565         writer.writeStartElement("section");
566         writer.writeAttribute("ref", href);
567         if (node->parent() && !node->parent()->name().isEmpty())
568             writer.writeAttribute(
569                     "title", tr("%1::%2 %3 Reference").arg(node->parent()->name()).arg(objName).arg(typeStr));
570         else
571             writer.writeAttribute("title", tr("%1 %2 Reference").arg(objName).arg(typeStr));
572 
573         addMembers(project, writer, node);
574         writer.writeEndElement(); // section
575     } break;
576 
577     case Node::Namespace:
578         writeSection(writer, href, objName);
579         break;
580 
581     case Node::Example:
582     case Node::HeaderFile:
583     case Node::Page:
584     case Node::Group:
585     case Node::Module:
586     case Node::JsModule:
587     case Node::QmlModule: {
588         writer.writeStartElement("section");
589         writer.writeAttribute("ref", href);
590         writer.writeAttribute("title", node->fullTitle());
591         if (node->nodeType() == Node::HeaderFile)
592             addMembers(project, writer, node);
593         writer.writeEndElement(); // section
594     } break;
595     default:;
596     }
597 }
598 
generateProject(HelpProject & project)599 void HelpProjectWriter::generateProject(HelpProject &project)
600 {
601     const Node *rootNode;
602 
603     // Restrict searching only to the local (primary) tree
604     QVector<Tree *> searchOrder = qdb_->searchOrder();
605     qdb_->setLocalSearch();
606 
607     if (!project.indexRoot.isEmpty())
608         rootNode = qdb_->findPageNodeByTitle(project.indexRoot);
609     else
610         rootNode = qdb_->primaryTreeRoot();
611 
612     if (rootNode == nullptr)
613         return;
614 
615     project.files.clear();
616     project.keywords.clear();
617 
618     QFile file(outputDir + QDir::separator() + project.fileName);
619     if (!file.open(QFile::WriteOnly | QFile::Text))
620         return;
621 
622     QXmlStreamWriter writer(&file);
623     writer.setAutoFormatting(true);
624     writer.writeStartDocument();
625     writer.writeStartElement("QtHelpProject");
626     writer.writeAttribute("version", "1.0");
627 
628     // Write metaData, virtualFolder and namespace elements.
629     writer.writeTextElement("namespace", project.helpNamespace);
630     writer.writeTextElement("virtualFolder", project.virtualFolder);
631     writer.writeStartElement("metaData");
632     writer.writeAttribute("name", "version");
633     writer.writeAttribute("value", project.version);
634     writer.writeEndElement();
635 
636     // Write customFilter elements.
637     for (auto it = project.customFilters.constBegin(); it != project.customFilters.constEnd();
638          ++it) {
639         writer.writeStartElement("customFilter");
640         writer.writeAttribute("name", it.key());
641         QStringList sortedAttributes = it.value().values();
642         sortedAttributes.sort();
643         for (const auto &filter : qAsConst(sortedAttributes))
644             writer.writeTextElement("filterAttribute", filter);
645         writer.writeEndElement(); // customFilter
646     }
647 
648     // Start the filterSection.
649     writer.writeStartElement("filterSection");
650 
651     // Write filterAttribute elements.
652     QStringList sortedFilterAttributes = project.filterAttributes.values();
653     sortedFilterAttributes.sort();
654     for (const auto &filterName : qAsConst(sortedFilterAttributes))
655         writer.writeTextElement("filterAttribute", filterName);
656 
657     writer.writeStartElement("toc");
658     writer.writeStartElement("section");
659     const Node *node = qdb_->findPageNodeByTitle(project.indexTitle);
660     if (node == nullptr)
661         node = qdb_->findNodeByNameAndType(QStringList("index.html"), &Node::isPageNode);
662     QString indexPath;
663     if (node)
664         indexPath = gen_->fullDocumentLocation(node, false);
665     else
666         indexPath = "index.html";
667     writer.writeAttribute("ref", indexPath);
668     writer.writeAttribute("title", project.indexTitle);
669 
670     generateSections(project, writer, rootNode);
671 
672     for (int i = 0; i < project.subprojects.length(); i++) {
673         SubProject subproject = project.subprojects[i];
674 
675         if (subproject.type == QLatin1String("manual")) {
676 
677             const Node *indexPage = qdb_->findNodeForTarget(subproject.indexTitle, nullptr);
678             if (indexPage) {
679                 Text indexBody = indexPage->doc().body();
680                 const Atom *atom = indexBody.firstAtom();
681                 QStack<int> sectionStack;
682                 bool inItem = false;
683 
684                 while (atom) {
685                     switch (atom->type()) {
686                     case Atom::ListLeft:
687                         sectionStack.push(0);
688                         break;
689                     case Atom::ListRight:
690                         if (sectionStack.pop() > 0)
691                             writer.writeEndElement(); // section
692                         break;
693                     case Atom::ListItemLeft:
694                         inItem = true;
695                         break;
696                     case Atom::ListItemRight:
697                         inItem = false;
698                         break;
699                     case Atom::Link:
700                         if (inItem) {
701                             if (sectionStack.top() > 0)
702                                 writer.writeEndElement(); // section
703 
704                             const Node *page = qdb_->findNodeForTarget(atom->string(), nullptr);
705                             writer.writeStartElement("section");
706                             QString indexPath = gen_->fullDocumentLocation(page, false);
707                             writer.writeAttribute("ref", indexPath);
708                             QString title = atom->string();
709                             if (atom->next() && atom->next()->string() == ATOM_FORMATTING_LINK)
710                                 if (atom->next()->next())
711                                     title = atom->next()->next()->string();
712                             writer.writeAttribute("title", title);
713 
714                             sectionStack.top() += 1;
715                         }
716                         break;
717                     default:;
718                     }
719 
720                     if (atom == indexBody.lastAtom())
721                         break;
722                     atom = atom->next();
723                 }
724             } else
725                 rootNode->doc().location().warning(
726                         tr("Failed to find index: %1").arg(subproject.indexTitle));
727 
728         } else {
729 
730             writer.writeStartElement("section");
731             QString indexPath = gen_->fullDocumentLocation(
732                     qdb_->findNodeForTarget(subproject.indexTitle, nullptr), false);
733             writer.writeAttribute("ref", indexPath);
734             writer.writeAttribute("title", subproject.title);
735 
736             if (subproject.sortPages) {
737                 QStringList titles = subproject.nodes.keys();
738                 titles.sort();
739                 for (const auto &title : qAsConst(titles)) {
740                     writeNode(project, writer, subproject.nodes[title]);
741                 }
742             } else {
743                 // Find a contents node and navigate from there, using the NextLink values.
744                 QSet<QString> visited;
745                 bool contentsFound = false;
746                 for (const auto *node : qAsConst(subproject.nodes)) {
747                     QString nextTitle = node->links().value(Node::NextLink).first;
748                     if (!nextTitle.isEmpty()
749                         && node->links().value(Node::ContentsLink).first.isEmpty()) {
750 
751                         const Node *nextPage = qdb_->findNodeForTarget(nextTitle, nullptr);
752 
753                         // Write the contents node.
754                         writeNode(project, writer, node);
755                         contentsFound = true;
756 
757                         while (nextPage) {
758                             writeNode(project, writer, nextPage);
759                             nextTitle = nextPage->links().value(Node::NextLink).first;
760                             if (nextTitle.isEmpty() || visited.contains(nextTitle))
761                                 break;
762                             nextPage = qdb_->findNodeForTarget(nextTitle, nullptr);
763                             visited.insert(nextTitle);
764                         }
765                         break;
766                     }
767                 }
768                 // No contents/nextpage links found, write all nodes unsorted
769                 if (!contentsFound) {
770                     QList<const Node *> subnodes = subproject.nodes.values();
771 
772                     std::sort(subnodes.begin(), subnodes.end(), Node::nodeNameLessThan);
773 
774                     for (const auto *node : qAsConst(subnodes))
775                         writeNode(project, writer, node);
776                 }
777             }
778 
779             writer.writeEndElement(); // section
780         }
781     }
782 
783     // Restore original search order
784     qdb_->setSearchOrder(searchOrder);
785 
786     writer.writeEndElement(); // section
787     writer.writeEndElement(); // toc
788 
789     writer.writeStartElement("keywords");
790     std::sort(project.keywords.begin(), project.keywords.end());
791     for (const QStringList &details : qAsConst(project.keywords)) {
792         writer.writeStartElement("keyword");
793         writer.writeAttribute("name", details[0]);
794         writer.writeAttribute("id", details[1]);
795         writer.writeAttribute("ref", details[2]);
796         writer.writeEndElement(); // keyword
797     }
798     writer.writeEndElement(); // keywords
799 
800     writer.writeStartElement("files");
801 
802     // The list of files to write is the union of generated files and
803     // other files (images and extras) included in the project
804     QSet<QString> files =
805             QSet<QString>(gen_->outputFileNames().cbegin(), gen_->outputFileNames().cend());
806     files.unite(project.files);
807     files.unite(project.extraFiles);
808     QStringList sortedFiles = files.values();
809     sortedFiles.sort();
810     for (const auto &usedFile : qAsConst(sortedFiles)) {
811         if (!usedFile.isEmpty())
812             writer.writeTextElement("file", usedFile);
813     }
814     writer.writeEndElement(); // files
815 
816     writer.writeEndElement(); // filterSection
817     writer.writeEndElement(); // QtHelpProject
818     writer.writeEndDocument();
819     writeHashFile(file);
820     file.close();
821 }
822 
823 QT_END_NAMESPACE
824