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