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