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

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