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

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