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