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
nodeextractor.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 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 "aggregate.h"
7#include "atom.h"
8#include "classnode.h"
9#include "codemarker.h"
10#include "collectionnode.h"
12#include "config.h"
13#include "doc.h"
14#include "enumnode.h"
15#include "functionnode.h"
16#include "generator.h"
17#include "hrefresolver.h"
19#include "namespacenode.h"
21#include "ir/contentbuilder.h"
22#include "ir/signaturespan.h"
23#include "pagenode.h"
24#include "parameters.h"
25#include "propertynode.h"
27#include "qdocdatabase.h"
28#include "qmltypenode.h"
29#include "sections.h"
32#include "text.h"
33#include "tree.h"
34#include "typedefnode.h"
35#include "utilities.h"
36#include "textutils.h"
37#include "variablenode.h"
38
39#include "location.h"
40
41#include <QHash>
42#include <QRegularExpression>
43#include <QSet>
44
45#include <deque>
46#include <optional>
47
48QT_BEGIN_NAMESPACE
49
50using namespace Qt::Literals;
51
53{
54 const Location &loc = node->doc().location();
55 return [loc](QtMsgType type, const QString &message) {
56 switch (type) {
57 case QtWarningMsg:
58 loc.warning(message);
59 break;
60 default:
61 loc.warning(message);
62 break;
63 }
64 };
65}
66
67static QString resolveHref(const HrefResolver *resolver, const Node *target, const Node *relative)
68{
69 if (!resolver)
70 return target->url();
71 auto result = resolver->hrefForNode(target, relative);
72 if (const auto *href = std::get_if<QString>(&result))
73 return *href;
74 return {};
75}
76
77namespace {
78
79struct DeprecationData
80{
81 QString deprecatedSince;
82 QString typeNoun;
83 QString deprecatedSinceQualified;
84};
85
86/*!
87 \internal
88
89 Returns the deprecation sentence data for \a node if the node is
90 deprecated or active with a nonempty deprecation version. Otherwise,
91 returns \c{std::nullopt}.
92
93 The result contains the type noun and the plain deprecation version.
94 For a QML node with a logical module name, \c{deprecatedSinceQualified}
95 prefixes the nonempty version with the module name. For other nodes,
96 it contains the plain version. Deprecated sentences use this qualified
97 version; scheduled sentences use the plain \c{deprecatedSince} version.
98
99 A deprecated node without a version still returns sentence data with
100 a type noun and two empty version strings.
101*/
102std::optional<DeprecationData> extractDeprecationData(const Node *node)
103{
104 const auto status = node->status();
105 const auto &version = node->deprecatedSince();
106 if (status != Status::Deprecated && (status != Status::Active || version.isEmpty()))
107 return std::nullopt;
108
109 DeprecationData data{version, NodeText::nodeTypeDescription(node), version};
110 if (!version.isEmpty() && node->isQmlNode() && !node->logicalModuleName().isEmpty())
111 data.deprecatedSinceQualified = node->logicalModuleName() + ' '_L1 + version;
112 return data;
113}
114
115// Locate a documented \concept node by name. Walks the QDocDatabase search
116// order so cross-module references resolve through dependency index trees.
117// The relative node is reserved for relative-node-aware scoping in a future
118// revision; today the lookup is global.
119const Node *findConceptNodeForRelative(const Node *relative, const QString &name)
120{
121 Q_UNUSED(relative);
123 if (!qdb)
124 return nullptr;
125 return qdb->findConceptNode(name);
126}
127
128// Tokenize a requires-clause text against a list of documented concept
129// names, emitting a Text/Link/Text/... span sequence into \a out. Mirrors
130// the Text+ExternalRef+Text span pattern used for external references, but
131// uses SpanRole::Link with an HrefResolver-resolved href instead of an
132// external cppreference URL. Used by both the template-head requires
133// clause site (inside buildTemplateDeclSpans) and the trailing requires
134// site (inside buildCppSynopsisSpans).
135//
136// Concept names are matched longest first, so a fully-qualified
137// "ns::Integral" wins against a bare "Integral" when both occur in the
138// same requires clause text. Each matched spelling maps back to its
139// fully-qualified name for lookup, so source text that spells a concept
140// either qualified or unqualified resolves to the same concept page.
141//
142// When no concept name appears in the text, the whole clause is emitted as
143// a single Text span. Concept identifiers are alphanumeric and underscore
144// per the C++ grammar, but the alternation is built through
145// QRegularExpression::escape() for defense in depth.
146void appendRequiresSpansWithLinks(QList<IR::SignatureSpan> &out,
147 const QString &requiresText,
148 const QStringList &conceptNames,
149 const HrefResolver *hrefResolver,
150 const Node *relative)
151{
152 auto emitFallback = [&out, &requiresText]() {
153 IR::SignatureSpan req;
155 req.text = " requires "_L1 + requiresText;
156 out.append(req);
157 };
158
159 if (conceptNames.isEmpty() || requiresText.isEmpty()) {
160 emitFallback();
161 return;
162 }
163
164 QStringList sorted = conceptNames;
165 std::sort(sorted.begin(), sorted.end(),
166 [](const QString &a, const QString &b) { return a.size() > b.size(); });
167
168 // Build a single alternation regex matching any concept name (longest
169 // first) anchored at word boundaries, so longer names take priority and
170 // overlapping matches don't double-emit. Each matched spelling — the
171 // fully-qualified name or its unqualified tail — maps back to the
172 // fully-qualified name the lookup needs.
173 QHash<QString, QString> targetForDisplay;
174 QStringList escapedAlternatives;
175 escapedAlternatives.reserve(sorted.size() * 2);
176 for (const QString &name : sorted) {
177 if (!targetForDisplay.contains(name)) {
178 targetForDisplay.insert(name, name);
179 escapedAlternatives.append(QRegularExpression::escape(name));
180 }
181 const QString tail = name.section("::"_L1, -1);
182 if (tail != name && !targetForDisplay.contains(tail)) {
183 targetForDisplay.insert(tail, name);
184 escapedAlternatives.append(QRegularExpression::escape(tail));
185 }
186 }
187 const QRegularExpression conceptRegex(
188 u"\\b("_s + escapedAlternatives.join(u"|"_s) + u")\\b"_s);
189 if (!conceptRegex.isValid()) {
190 emitFallback();
191 return;
192 }
193
194 auto it = conceptRegex.globalMatch(requiresText);
195 if (!it.hasNext()) {
196 emitFallback();
197 return;
198 }
199
200 // At least one concept matches: emit the " requires " lead-in, then
201 // alternate plain-text runs with concept spans.
202 IR::SignatureSpan lead;
204 lead.text = " requires "_L1;
205 out.append(lead);
206
207 qsizetype pos = 0;
208 while (it.hasNext()) {
209 const auto match = it.next();
210 if (match.capturedStart() > pos) {
211 IR::SignatureSpan textSpan;
212 textSpan.role = IR::SpanRole::Text;
213 textSpan.text = requiresText.mid(pos, match.capturedStart() - pos);
214 out.append(textSpan);
215 }
216 const QString matched = match.captured(1);
217 const QString target = targetForDisplay.value(matched, matched);
218 const Node *cn = findConceptNodeForRelative(relative, target);
219 const QString href = cn ? resolveHref(hrefResolver, cn, relative) : QString();
220
221 IR::SignatureSpan span;
222 span.text = matched;
223 if (href.isEmpty()) {
224 // An undocumented concept, or one whose page can't be located,
225 // renders as plain text rather than a dangling link.
227 } else {
229 span.href = href;
230 }
231 out.append(span);
232 pos = match.capturedEnd();
233 }
234 if (pos < requiresText.size()) {
235 IR::SignatureSpan tail;
237 tail.text = requiresText.mid(pos);
238 out.append(tail);
239 }
240}
241
242} // namespace
243
244namespace NodeExtractor {
245
246/*!
247 \internal
248 \typealias NodeExtractor::SemanticOrigins
249
250 Page-local, never serialized: for each emitted detail section, one entry
251 per emitted member in the same order, pointing at the node on which that
252 member's detail documentation was authored. For a regular member the
253 origin is the member itself; for a member of a SharedCommentNode group it
254 is the SharedCommentNode, because the copied body and see-also text was
255 authored in that comment. A member without an anchorId maps to nullptr,
256 which the link resolver treats as "resolve in page context".
257
258 The list is paired with the extracted detail-section and member order
259 rather than keyed by anchorId: emitted anchors are not unique, so two
260 members (such as a QML type's regular and attached signal of the same
261 name) can share one anchorId. The pairing travels beside the IR values,
262 not inside them.
263*/
264
265/*!
266 \internal
267 Extract page-level metadata from a PageNode into a value-type struct.
268
269 This function reads classification, identity, brief, and body fields
270 from the given PageNode and returns them as an IR::PageMetadata value.
271 Body content is populated via ContentBuilder, which transforms the
272 atom chain into structured content blocks. Format-conditional atoms
273 are skipped unconditionally since the template generator builds a
274 format-agnostic IR.
275
276 For aggregate pages (classes, QML types, namespaces), member listings
277 are extracted via the Sections infrastructure and stored as frozen
278 SectionIR values.
279
280 The caller (TemplateGenerator) invokes this before passing the
281 result to IR::Builder, ensuring Builder never includes PageNode
282 or other Node subclass headers.
283
284 \a origins, when non-null, is populated alongside detail extraction
285 with a page-local origin paired with each emitted detail member, in
286 section and member order, for the template post-processing that
287 resolves links in the authored member context.
288*/
289IR::PageMetadata extractPageMetadata(const PageNode *pn, const HrefResolver *hrefResolver,
290 SemanticOrigins *origins)
291{
292 Q_ASSERT_X(pn, "NodeExtractor::extractPageMetadata",
293 "PageNode pointer must be non-null");
294 IR::PageMetadata pm;
295
296 pm.nodeType = pn->nodeType();
297 pm.genus = pn->genus();
298 pm.status = pn->status();
299 pm.access = pn->access();
300
301 if (pn->isQmlType()) {
302 const auto *qcn = static_cast<const QmlTypeNode *>(pn);
303 QString suffix = qcn->isQmlBasicType() ? " QML Value Type"_L1 : " QML Type"_L1;
304 pm.title = pn->name() + suffix;
305 pm.fullTitle = pm.title;
306 } else if (pn->isClassNode() || pn->isNamespace() || pn->isHeader()) {
307 // plainFullName() produces qualified names for nested aggregates
308 // (e.g. "Outer::Inner"), matching the legacy generator behavior.
309 // For top-level types and headers the result is the same as name().
310 const auto *aggregate = static_cast<const Aggregate *>(pn);
311 pm.fullTitle = aggregate->plainFullName();
312 pm.title = pm.fullTitle;
313 } else {
314 pm.title = pn->title();
315 pm.fullTitle = pn->fullTitle();
316 }
317
318 pm.url = hrefResolver->fileName(pn);
319 const QString baseUrl = Config::instance().get(CONFIG_URL).asString();
320 if (!baseUrl.isEmpty() && !pm.url.isEmpty() && !pm.url.contains("://"_L1)) {
321 pm.url = baseUrl
322 + (baseUrl.endsWith(u'/') ? QString{} : QStringLiteral("/"))
323 + pm.url;
324 }
325 pm.since = pn->since();
326 pm.deprecatedSince = pn->deprecatedSince();
327 if (const auto deprecation = extractDeprecationData(pn)) {
328 pm.typeNoun = deprecation->typeNoun;
329 pm.deprecatedSinceQualified = deprecation->deprecatedSinceQualified;
330 }
331 pm.brief = pn->doc().briefText().toString();
332
333 const Text &bodyText = pn->doc().body();
334 if (const Atom *firstAtom = bodyText.firstAtom()) {
335 // Offset section heading levels to account for page structure.
336 // QDoc's \section1 maps to level 1, but pages already use <h1>
337 // for the title. The legacy generators apply a node-type-dependent
338 // offset; we replicate the same mapping here.
339 const int headingOffset = [&] {
340 switch (pn->nodeType()) {
342 case NodeType::Class:
343 case NodeType::Struct:
344 case NodeType::Union:
345 case NodeType::Module:
346 return 2;
350 case NodeType::Page:
351 case NodeType::Group:
352 return 1;
353 default:
354 return 3;
355 }
356 }();
357 IR::ContentBuilder contentBuilder(IR::BriefHandling::Skip, headingOffset,
358 diagnosticHandlerFor(pn));
359 pm.body = contentBuilder.build(firstAtom);
360 }
361
362 if (pn->isAggregate()) {
363 const auto *aggregate = static_cast<const Aggregate *>(pn);
364 pm.summarySections = extractSummarySections(aggregate, hrefResolver);
365 pm.detailSections = extractDetailSections(aggregate, hrefResolver, origins);
366 }
367
368 if (pn->isQmlType()) {
369 const auto *qcn = static_cast<const QmlTypeNode *>(pn);
370 pm.qmlTypeData = extractQmlTypeData(qcn, hrefResolver);
371 }
372
373 if (pn->isCollectionNode()) {
374 const auto *cn = static_cast<const CollectionNode *>(pn);
375 pm.collectionData = extractCollectionData(cn, hrefResolver);
376 }
377
378 if (pn->isClassNode() || pn->isNamespace() || pn->isHeader()) {
379 const auto *aggregate = static_cast<const Aggregate *>(pn);
380 pm.cppReferenceData = extractCppReferenceData(aggregate, hrefResolver);
381 }
382
383 pm.navigationData = extractNavigationData(pn, hrefResolver);
384
385 return pm;
386}
387
388/*!
389 \internal
390 Extract QML type metadata from a QmlTypeNode.
391
392 Populates import statement, inheritance chain, inherited-by list,
393 native C++ type link, and singleton/value-type flags. InclusionFilter
394 is applied to match the legacy generator's visibility filtering.
395
396 hasObsoleteMembers reports whether any section holds deprecated
397 members, the same check the legacy QML generator applies before
398 generating its obsolete sub-page. The check walks the section
399 distribution rather than only direct children so members inherited
400 from an abstract base type count as well.
401*/
402IR::QmlTypeData extractQmlTypeData(const QmlTypeNode *qcn, const HrefResolver *hrefResolver)
403{
404 IR::QmlTypeData data;
405 const InclusionPolicy policy = Config::instance().createInclusionPolicy();
406
407 if (!qcn->logicalModuleName().isEmpty()) {
408 bool includeImport = true;
409 const CollectionNode *collection = qcn->logicalModule();
410 if (collection) {
411 const NodeContext context = collection->createContext();
412 includeImport = InclusionFilter::isIncluded(policy, context);
413 }
414 if (includeImport) {
415 QStringList parts = QStringList()
416 << "import"_L1 << qcn->logicalModuleName() << qcn->logicalModuleVersion();
417 data.importStatement = parts.join(' '_L1).trimmed();
418 }
419 }
420
423
424 QmlTypeNode *base = qcn->qmlBaseNode();
425 while (base) {
426 const NodeContext context = base->createContext();
427 if (InclusionFilter::isIncluded(policy, context))
428 break;
429 base = base->qmlBaseNode();
430 }
431
432 NodeList subs;
433 QmlTypeNode::subclasses(qcn, subs, true);
434
435 if (base) {
436 IR::QmlTypeData::InheritsInfo inheritsInfo;
437 inheritsInfo.name = base->name();
438 inheritsInfo.href = resolveHref(hrefResolver, base, qcn);
439 const CollectionNode *baseModule = base->logicalModule();
440 if (baseModule) {
441 const NodeContext moduleContext = baseModule->createContext();
442 if (InclusionFilter::isIncluded(policy, moduleContext))
443 inheritsInfo.moduleName = base->logicalModuleName();
444 }
445 data.inherits = inheritsInfo;
446 }
447
448 if (!subs.isEmpty()) {
449 QList<IR::QmlTypeData::InheritedByEntry> filteredSubs;
450 for (const auto *sub : std::as_const(subs)) {
451 const NodeContext context = sub->createContext();
452 if (InclusionFilter::isIncluded(policy, context))
453 filteredSubs.append({sub->name(), resolveHref(hrefResolver, sub, qcn)});
454 }
455 std::sort(filteredSubs.begin(), filteredSubs.end(),
456 [](const IR::QmlTypeData::InheritedByEntry &a,
457 const IR::QmlTypeData::InheritedByEntry &b) {
458 return a.name < b.name;
459 });
460 data.inheritedBy = filteredSubs;
461 }
462
463 ClassNode *cn = qcn->classNode();
464 if (cn && cn->isQmlNativeType()) {
465 const NodeContext context = cn->createContext();
466 if (InclusionFilter::isIncluded(policy, context))
467 data.nativeType = IR::QmlTypeData::NativeTypeInfo{cn->name(), resolveHref(hrefResolver, cn, qcn)};
468 }
469
470 SectionPtrVector obsoleteSummary;
471 SectionPtrVector obsoleteDetail;
472 Sections sections(qcn);
473 data.hasObsoleteMembers = sections.hasObsoleteMembers(&obsoleteSummary, &obsoleteDetail);
474
475 return data;
476}
477
478/*!
479 \internal
480 Extract collection metadata from a CollectionNode.
481
482 Populates module identity, CMake/qmake build variables, technology
483 preview state, and pre-sorted member listings. For C++ modules,
484 members are categorized into separate namespace and class lists.
485 For groups and QML modules, a single flat member list is produced.
486
487 All member lists are filtered through InclusionFilter (excluding
488 internal entries) and exclude deprecated nodes, then sorted
489 alphabetically by name (case-insensitive).
490*/
491IR::CollectionData extractCollectionData(const CollectionNode *cn, const HrefResolver *hrefResolver)
492{
493 IR::CollectionData data;
494
495 data.logicalModuleName = cn->logicalModuleName();
496 data.logicalModuleVersion = cn->logicalModuleVersion();
497 data.qtVariable = cn->qtVariable();
498
499 // CMake metadata synthesis. Documenters can declare CMake info
500 // through several command shapes (\cmakepackage, \cmakecomponent,
501 // \cmaketargetitem and the Qt-specific \qtcmakepackage and
502 // \qtcmaketargetitem) that populate different combinations of
503 // the package, component and target fields on the CollectionNode.
504 // Synthesize the package as Qt<major-version> when only a
505 // component is declared, so module pages render coherent
506 // find_package snippets for both the generic and Qt-flavored
507 // command families. The target_link_libraries line is
508 // synthesized only when a component is also available — a bare
509 // package name does not imply a target named package::package,
510 // so packages without a component leave the target empty and
511 // the template suppresses the target_link_libraries clause.
512 // When no field is set, leave the data fields empty so the
513 // template suppresses the CMake row entirely.
514 const QString rawCmakePackage = cn->cmakePackage();
515 const QString rawCmakeComponent = cn->cmakeComponent();
516 const QString rawCmakeTargetItem = cn->cmakeTargetItem();
517 if (!rawCmakePackage.isEmpty() || !rawCmakeComponent.isEmpty()) {
518 const QString package = rawCmakePackage.isEmpty()
519 ? "Qt"_L1 + QString::number(QT_VERSION_MAJOR)
520 : rawCmakePackage;
521 data.cmakePackage = package;
522 data.cmakeComponent = rawCmakeComponent;
523 if (!rawCmakeTargetItem.isEmpty())
524 data.cmakeTargetItem = rawCmakeTargetItem;
525 else if (!rawCmakeComponent.isEmpty())
526 data.cmakeTargetItem = package + "::"_L1 + rawCmakeComponent;
527 }
528
529 data.state = cn->state();
530
531 data.isModule = cn->isModule();
532 data.isQmlModule = cn->isQmlModule();
533 data.isGroup = cn->isGroup();
534 data.isConcept = cn->isConcept();
535 data.noAutoList = cn->noAutoList();
536
537 if (cn->noAutoList())
538 return data;
539
540 const InclusionPolicy policy = Config::instance().createInclusionPolicy();
541
542 auto makeMemberEntry = [hrefResolver, cn](const Node *node) -> IR::CollectionData::MemberEntry {
543 return { node->name(), resolveHref(hrefResolver, node, cn), node->doc().briefText().toString() };
544 };
545
546 auto sortEntries = [](QList<IR::CollectionData::MemberEntry> &entries) {
547 std::sort(entries.begin(), entries.end(),
548 [](const IR::CollectionData::MemberEntry &a,
549 const IR::CollectionData::MemberEntry &b) {
550 return a.name.compare(b.name, Qt::CaseInsensitive) < 0;
551 });
552 };
553
554 if (cn->isModule()) {
556 for (auto *node : nsMap.values()) {
557 const NodeContext context = node->createContext();
558 if (InclusionFilter::isIncluded(policy, context) && !node->isDeprecated())
559 data.namespaces.append(makeMemberEntry(node));
560 }
561 sortEntries(data.namespaces);
562
563 const NodeMap classMap = cn->getMembers([](const Node *n) { return n->isClassNode(); });
564 for (auto *node : classMap.values()) {
565 const NodeContext context = node->createContext();
566 if (InclusionFilter::isIncluded(policy, context) && !node->isDeprecated())
567 data.classes.append(makeMemberEntry(node));
568 }
569 sortEntries(data.classes);
570 } else {
571 for (const auto *node : cn->members()) {
572 if (!node->isInAPI())
573 continue;
574 const NodeContext context = node->createContext();
575 if (InclusionFilter::isIncluded(policy, context) && !node->isDeprecated())
576 data.members.append(makeMemberEntry(node));
577 }
578 sortEntries(data.members);
579 }
580
581 return data;
582}
583
585 const HrefResolver *hrefResolver,
586 const Node *relative);
587
588/*!
589 \internal
590 Extract C++ reference page metadata from a class, namespace, or header.
591
592 Reads requisite table fields (header include, build-system snippets,
593 status), inheritance hierarchies, template declarations, comparison
594 operators, thread-safeness, and group associations. The result is a
595 value-type struct that captures everything the template generator needs
596 to render the requisites table and secondary sections without touching
597 the Node tree at render time.
598
599 All three aggregate page types (ClassNode, NamespaceNode, HeaderNode)
600 are handled, with ClassNode-specific sections gated on isClassNode().
601*/
602IR::CppReferenceData extractCppReferenceData(const Aggregate *aggregate, const HrefResolver *hrefResolver)
603{
604 IR::CppReferenceData data;
606
607 data.isNamespace = aggregate->isNamespace();
608 data.isHeader = aggregate->isHeader();
609 data.isInnerClass = aggregate->parent() && aggregate->parent()->isClassNode();
610 data.typeWord = aggregate->typeWord(false);
612
613 auto ancestors = aggregate->plainFullName().split("::"_L1);
614 ancestors.pop_back();
615 data.ancestorNames = ancestors;
616
617 if (aggregate->includeFile())
618 data.headerInclude = *aggregate->includeFile();
619
620 if (!aggregate->physicalModuleName().isEmpty()) {
621 const CollectionNode *cn =
622 qdb->getCollectionNode(aggregate->physicalModuleName(), NodeType::Module);
623 if (cn && (!cn->cmakeComponent().isEmpty() || !cn->cmakePackage().isEmpty())) {
624 const QString package = cn->cmakePackage().isEmpty()
625 ? "Qt"_L1 + QString::number(QT_VERSION_MAJOR)
626 : cn->cmakePackage();
627 QString findPkg;
628 if (cn->cmakeComponent().isEmpty())
629 findPkg = "find_package("_L1 + package + " REQUIRED)"_L1;
630 else
631 findPkg = "find_package("_L1 + package + " REQUIRED COMPONENTS "_L1
632 + cn->cmakeComponent() + ")"_L1;
633
634 QString target;
635 if (!cn->cmakeTargetItem().isEmpty()) {
636 target = cn->cmakeTargetItem();
637 } else if (cn->cmakeComponent().isEmpty()) {
638 target = package + "::"_L1 + package;
639 } else {
640 target = package + "::"_L1 + cn->cmakeComponent();
641 }
642
643 data.cmakeFindPackage = findPkg;
644 data.cmakeTargetLinkLibraries =
645 "target_link_libraries(mytarget PRIVATE "_L1 + target + ")"_L1;
646 }
647 if (cn && !cn->qtVariable().isEmpty())
648 data.qmakeVariable = "QT += "_L1 + cn->qtVariable();
649 }
650
651 auto statusOpt = formatStatus(aggregate, qdb);
652 if (statusOpt) {
653 data.statusText = *statusOpt;
654 if (aggregate->status() == Status::Deprecated)
655 data.statusCssClass = "deprecated"_L1;
656 else if (!aggregate->deprecatedSince().isEmpty())
657 data.statusCssClass = "pending-deprecation"_L1;
658 else if (aggregate->status() == Status::Preliminary)
659 data.statusCssClass = "preliminary"_L1;
660 else
661 data.statusCssClass = "status"_L1;
662 }
663
664 if (aggregate->isClassNode()) {
665 auto *classNode = const_cast<ClassNode *>(static_cast<const ClassNode *>(aggregate));
666
667 if (classNode->isQmlNativeType()) {
668 const InclusionPolicy policy = Config::instance().createInclusionPolicy();
669 const NodeContext context = classNode->createContext();
670 if (InclusionFilter::isIncluded(policy, context)) {
671 QList<QmlTypeNode *> nativeTypes{classNode->qmlNativeTypes().cbegin(),
672 classNode->qmlNativeTypes().cend()};
673 if (!nativeTypes.isEmpty()) {
674 std::sort(nativeTypes.begin(), nativeTypes.end(), Node::nodeNameLessThan);
675 data.qmlNativeType = IR::CppReferenceData::QmlNativeTypeLink{
676 nativeTypes.first()->name(),
677 resolveHref(hrefResolver, nativeTypes.first(), aggregate)
678 };
679 }
680 }
681 }
682
683 const auto *metaTags = classNode->doc().metaTagMap();
684 if (metaTags && metaTags->contains(u"qdoc-suppress-inheritance"_s))
685 data.suppressInheritance = true;
686
687 if (!data.suppressInheritance) {
688 const auto &baseClasses = classNode->baseClasses();
689 for (const auto &bc : baseClasses) {
690 if (bc.m_node) {
691 data.baseClasses.append({
692 bc.m_node->plainFullName(),
693 resolveHref(hrefResolver, bc.m_node, aggregate),
694 bc.m_access
695 });
696 }
697 }
698
699 const auto &derivedClasses = classNode->derivedClasses();
700 for (const auto &dc : derivedClasses) {
701 if (dc.m_node) {
702 data.derivedClasses.append({
703 dc.m_node->plainFullName(),
704 resolveHref(hrefResolver, dc.m_node, aggregate)
705 });
706 }
707 }
708 std::sort(data.derivedClasses.begin(), data.derivedClasses.end(),
711 return a.name.compare(b.name, Qt::CaseInsensitive) < 0;
712 });
713 }
714 }
715
716 if (aggregate->templateDecl()) {
717 const auto &templateDecl = *aggregate->templateDecl();
718 data.templateDeclSpans = buildTemplateDeclSpans(&templateDecl, hrefResolver, aggregate);
719 data.referencedConcepts.reserve(int(templateDecl.referenced_concepts.size()));
720 for (const auto &s : templateDecl.referenced_concepts)
721 data.referencedConcepts.append(QString::fromStdString(s));
722 }
723
724 const auto selfCategory = aggregate->comparisonCategory();
725 if (selfCategory != ComparisonCategory::None)
726 data.selfComparisonCategory = QString::fromStdString(comparisonCategoryAsString(selfCategory));
727
728 const auto *comparesMap = aggregate->doc().comparesWithMap();
729 if (comparesMap && !comparesMap->isEmpty()) {
730 for (auto [key, description] : comparesMap->asKeyValueRange()) {
731 IR::CppReferenceData::ComparisonEntry entry;
732 entry.category = QString::fromStdString(comparisonCategoryAsString(key));
733
734 const QStringList types{description.firstAtom()->string().split(';'_L1)};
735 entry.comparableTypes = types;
736
737 if (description.firstAtom()->next() != description.lastAtom()) {
738 Text descText = Text::subText(description.firstAtom()->next(),
739 description.lastAtom());
740 entry.description = descText.toString();
741 }
742 data.comparisonEntries.append(entry);
743 }
744 }
745
746 Node::ThreadSafeness ts = aggregate->threadSafeness();
747 if (ts != Node::UnspecifiedSafeness) {
749 switch (ts) {
751 tsInfo.level = "non-reentrant"_L1;
752 break;
753 case Node::Reentrant:
754 tsInfo.level = "reentrant"_L1;
755 break;
756 case Node::ThreadSafe:
757 tsInfo.level = "thread-safe"_L1;
758 break;
759 default:
760 break;
761 }
762
763 NodeList reentrant, threadsafe, nonreentrant;
764 bool hasExceptions = false;
765 for (const auto *child : aggregate->childNodes()) {
766 if (!child->isDeprecated()) {
767 switch (child->threadSafeness()) {
768 case Node::Reentrant:
769 reentrant.append(const_cast<Node *>(child));
770 if (ts == Node::ThreadSafe) hasExceptions = true;
771 break;
772 case Node::ThreadSafe:
773 threadsafe.append(const_cast<Node *>(child));
774 if (ts == Node::Reentrant) hasExceptions = true;
775 break;
776 case Node::NonReentrant:
777 nonreentrant.append(const_cast<Node *>(child));
778 hasExceptions = true;
779 break;
780 default:
781 break;
782 }
783 }
784 }
785 if (hasExceptions) {
786 for (const auto *node : std::as_const(reentrant)) {
787 tsInfo.reentrantExceptions.append({
788 node->plainFullName(),
789 resolveHref(hrefResolver, node, aggregate)
790 });
791 }
792 for (const auto *node : std::as_const(threadsafe)) {
793 tsInfo.threadSafeExceptions.append({
794 node->plainFullName(),
795 resolveHref(hrefResolver, node, aggregate)
796 });
797 }
798 for (const auto *node : std::as_const(nonreentrant)) {
799 tsInfo.nonReentrantExceptions.append({
800 node->plainFullName(),
801 resolveHref(hrefResolver, node, aggregate)
802 });
803 }
804 }
805 data.threadSafety = std::move(tsInfo);
806 }
807
808 const QStringList &groupNames = aggregate->groupNames();
809 if (!groupNames.isEmpty()) {
810 const auto &groupMap = qdb->groups();
811 for (const auto &groupName : groupNames) {
812 auto it = groupMap.find(groupName);
813 if (it == groupMap.end() || !*it)
814 continue;
815 CollectionNode *group = *it;
816 // TODO: mergeCollections() mutates the node tree during
817 // extraction, violating the principle that the new pipeline
818 // reads without side effects. Replace with an eager merge
819 // pass that runs before generation begins.
820 qdb->mergeCollections(group);
821 if (group->wasSeen()) {
822 data.groups.append({
823 group->fullTitle(),
824 resolveHref(hrefResolver, group, aggregate)
825 });
826 }
827 }
828 }
829
830 if (aggregate->isNamespace()) {
831 const auto *ns = static_cast<const NamespaceNode *>(aggregate);
832 if (!ns->hasDoc() && ns->docNode()) {
833 data.isPartialNamespace = true;
834 data.fullNamespaceHref = resolveHref(hrefResolver, ns->docNode(), aggregate);
835 data.fullNamespaceModuleName = ns->docNode()->tree()->camelCaseModuleName();
836 }
837 }
838
839 return data;
840}
841
842/*!
843 \internal
844 Build categorized summary sections for an aggregate node.
845
846 Delegates to the Sections class for member distribution, then extracts
847 results into frozen SectionIR values. The section variant (C++ class,
848 QML type, or generic) is chosen based on the aggregate's node type.
849*/
850QList<IR::SectionIR> extractSummarySections(const Aggregate *aggregate, const HrefResolver *hrefResolver)
851{
852 Sections sections(aggregate);
853
854 const auto &sv = sections.summarySections();
855
856 QList<IR::SectionIR> result;
857 for (const auto &section : sv) {
858 if (section.isEmpty())
859 continue;
860
861 IR::SectionIR irSection;
862 irSection.title = section.title();
863 irSection.id = TextUtils::asAsciiPrintable(section.title());
864 irSection.singular = section.singular();
865 irSection.plural = section.plural();
866
867 // SharedCommentNode groups several declarations under one doc comment.
868 // Expand them into individual MemberIR entries so each function appears
869 // in the member table. This means the IR member count may exceed the
870 // Section::members() count.
871 for (const auto *member : section.members()) {
872 if (member->isSharedCommentNode()) {
873 const auto *scn = static_cast<const SharedCommentNode *>(member);
874 for (const auto *child : scn->collective()) {
875 IR::MemberIR irMember = extractMemberIR(child, hrefResolver, aggregate);
876 irMember.href = "#"_L1 + hrefResolver->anchorForNode(child);
877 irSection.members.append(irMember);
878 }
879 } else {
880 IR::MemberIR irMember = extractMemberIR(member, hrefResolver, aggregate);
881 irMember.href = "#"_L1 + hrefResolver->anchorForNode(member);
882 irSection.members.append(irMember);
883 }
884 }
885
886 for (const auto *reimpl : section.reimplementedMembers())
887 irSection.reimplementedMembers.append(extractMemberIR(reimpl, hrefResolver, aggregate));
888
889 for (const auto &[base, count] : section.inheritedMembers()) {
890 IR::InheritedMembersIR inherited;
891 inherited.className = base->plainFullName();
892 inherited.count = count;
893 inherited.href = resolveHref(hrefResolver, base, aggregate);
894 irSection.inheritedMembers.append(inherited);
895 }
896
897 result.append(irSection);
898 }
899 return result;
900}
901
902/*!
903 \internal
904 Build categorized detail sections for an aggregate node.
905
906 Iterates Sections::detailsSections() and extracts full member
907 documentation including body content, anchor IDs, and metadata.
908 SharedCommentNode groups share a single documentation body across
909 their children; the body and see-also list are stored once on the
910 section's shared detail group, which references its members
911 positionally and carries the group's own anchor when it has one
912 (named QML property groups do), while each child keeps its own
913 anchorId, synopsis, and per-member fields with an empty body and
914 see-also list. Because one page fragment may own only one
915 destination, a group anchor that collides with a member anchor or
916 with an earlier group's anchor on the same page is dropped.
917
918 \a origins, when non-null, receives one entry per emitted detail
919 member, paired with the member order of each emitted section: the
920 node the member's detail documentation was authored on — the member
921 itself, or the SharedCommentNode for copied shared text. A member
922 without an anchorId maps to nullptr; its content resolves in page
923 context. Emitted anchors are not unique (a QML type's regular and
924 attached signal of the same name share one), so the pairing is
925 positional rather than keyed by anchorId.
926*/
927QList<IR::SectionIR> extractDetailSections(const Aggregate *aggregate, const HrefResolver *hrefResolver,
928 SemanticOrigins *origins)
929{
930 Sections sections(aggregate);
931 const auto &sv = sections.detailsSections();
932
933 QList<IR::SectionIR> result;
934 for (const auto &section : sv) {
935 if (section.isEmpty())
936 continue;
937
938 IR::SectionIR irSection;
939 irSection.title = section.title();
940 irSection.id = TextUtils::asAsciiPrintable(section.title());
941 irSection.singular = section.singular();
942 irSection.plural = section.plural();
943
944 // Paired with irSection.members: the node each member's detail
945 // documentation was authored on (nullptr without an anchor).
946 QList<const Node *> sectionOrigins;
947
948 for (const auto *member : section.members()) {
949 if (member->isSharedCommentNode()) {
950 const auto *scn = static_cast<const SharedCommentNode *>(member);
951
952 // The body and see-also list are authored on the shared
953 // comment, not on any one child, so they are stored once
954 // on the group, which the templates render after the
955 // group's last member.
956 IR::SharedDetailGroup group;
957 group.firstIndex = static_cast<int>(irSection.members.size());
958 group.count = static_cast<int>(scn->collective().size());
959 group.status = scn->status();
960 group.isPlural = !scn->isPropertyGroup() && group.count > 1;
961 // A named QML property group is itself a link target, so
962 // the group carries its own anchor (such as "font-prop")
963 // beside its children's. Anonymous shared comments have
964 // none; anchorForNode returns an empty string for them.
965 group.anchorId = hrefResolver->anchorForNode(scn);
966 if (const auto deprecation = extractDeprecationData(scn)) {
967 group.deprecatedSince = deprecation->deprecatedSince;
968 group.deprecatedSinceQualified = deprecation->deprecatedSinceQualified;
969 const Node *described = group.isPlural ? scn->collective().first() : scn;
970 const auto number = group.isPlural ? NodeText::GrammaticalNumber::Plural
971 : NodeText::GrammaticalNumber::Singular;
972 group.typeNoun = NodeText::nodeTypeDescription(described, number);
973 }
974
975 const Text &bodyText = scn->doc().body();
976 if (const Atom *firstAtom = bodyText.firstAtom()) {
977 IR::ContentBuilder contentBuilder(IR::BriefHandling::Include, 0,
978 diagnosticHandlerFor(scn));
979 group.body = contentBuilder.build(firstAtom);
980 }
981
982 const QList<Text> &alsoTexts = scn->doc().alsoList();
983 for (const Text &alsoText : alsoTexts) {
984 if (const Atom *firstAtom = alsoText.firstAtom()) {
985 IR::ContentBuilder contentBuilder(IR::BriefHandling::Include, 0,
986 diagnosticHandlerFor(scn));
987 group.alsoList.append(contentBuilder.build(firstAtom));
988 }
989 }
990
991 for (const auto *child : scn->collective()) {
992 IR::MemberIR irMember = extractMemberIR(child, hrefResolver, aggregate, MemberExtractionLevel::Detail);
993 // The child shares the group's comment; its body and
994 // see-also list belong to the group, which renders
995 // them once, so the member carries only the group's
996 // index.
997 irMember.body.clear();
998 irMember.alsoList.clear();
999 irMember.sharedGroup = static_cast<int>(irSection.sharedGroups.size());
1000 // The shared text was authored in the shared comment,
1001 // not on the child; resolve it in that context.
1002 sectionOrigins.append(irMember.anchorId.isEmpty() ? nullptr : scn);
1003 irSection.members.append(irMember);
1004 }
1005 irSection.sharedGroups.append(group);
1006 } else {
1007 IR::MemberIR irMember = extractMemberIR(member, hrefResolver, aggregate, MemberExtractionLevel::Detail);
1008 sectionOrigins.append(irMember.anchorId.isEmpty() ? nullptr : member);
1009 irSection.members.append(irMember);
1010 }
1011 }
1012
1013 if (origins)
1014 origins->append(std::move(sectionOrigins));
1015 result.append(irSection);
1016 }
1017
1018 // A named QML property group emits its own anchor (such as
1019 // "font-prop") beside its children's, but one page fragment may
1020 // own only one destination. An ordinary member documented on the
1021 // page keeps the anchor it already owns, and a group rendered
1022 // earlier keeps it over a later group with the same anchor, so a
1023 // colliding group anchor is dropped rather than duplicated.
1024 QSet<QString> ownedAnchors;
1025 for (const auto &section : result)
1026 for (const auto &member : section.members)
1027 if (!member.anchorId.isEmpty())
1028 ownedAnchors.insert(member.anchorId);
1029
1030 for (auto &section : result)
1031 for (auto &group : section.sharedGroups) {
1032 if (group.anchorId.isEmpty())
1033 continue;
1034 if (ownedAnchors.contains(group.anchorId))
1035 group.anchorId.clear();
1036 else
1037 ownedAnchors.insert(group.anchorId);
1038 }
1039
1040 return result;
1041}
1042
1044{
1045 switch (ts) {
1046 case Node::Reentrant:
1047 return "reentrant"_L1;
1048 case Node::ThreadSafe:
1049 return "thread-safe"_L1;
1050 default:
1051 return {};
1052 }
1053}
1054
1055/*!
1056 \internal
1057 Replace the text of each constant in the value tables of \a blocks with
1058 the value qualified to the enum's declaration scope, so that the
1059 template generators display the same constant names as the HTML, WebXML,
1060 and DocBook generators. Value tables of non-enum nodes, such as
1061 enumeration properties, keep their authored spelling.
1062*/
1063static void qualifyEnumValueTerms(const EnumNode *en, QList<IR::ContentBlock> &blocks)
1064{
1065 for (auto &block : blocks) {
1066 if (block.type == IR::BlockType::DefinitionList
1067 && block.attributes.value(QLatin1String("listType"))
1068 == QLatin1String(ATOM_LIST_VALUE)) {
1069 for (auto &entry : block.children) {
1070 if (entry.type != IR::BlockType::DefinitionTerm || entry.inlineContent.isEmpty())
1071 continue;
1072 QString raw;
1073 for (const IR::InlineContent &content : entry.inlineContent)
1074 raw += content.plainText();
1075 const QString qualified = en->qualifiedValueName(raw);
1076 if (qualified != raw) {
1077 IR::InlineContent text;
1078 text.type = IR::InlineType::Text;
1079 text.text = qualified;
1080 entry.inlineContent = { text };
1081 }
1082 }
1083 }
1084 qualifyEnumValueTerms(en, block.children);
1085 }
1086}
1087
1088/*!
1089 \internal
1090 Build a MemberIR from a single Node.
1091
1092 Extracts identity, classification, and type-specific data from the node.
1093 FunctionNode provides signatures, parameters, and overload metadata.
1094 EnumNode provides scoped/unscoped signature and enum value listings.
1095 PropertyNode provides a qualified data type signature.
1096
1097 When \a level is MemberExtractionLevel::Detail, also populates
1098 detail documentation fields: anchorId, synopsis, since,
1099 threadSafety, comparisonCategory, noexcept metadata, body (via
1100 ContentBuilder), and alsoList.
1101*/
1102IR::MemberIR extractMemberIR(const Node *node, const HrefResolver *hrefResolver, const Node *relative, MemberExtractionLevel level)
1103{
1104 const bool includeDetail = (level == MemberExtractionLevel::Detail);
1105 IR::MemberIR member;
1106
1107 member.name = node->name();
1108 member.fullName = node->plainFullName();
1109 member.href = resolveHref(hrefResolver, node, relative);
1110 member.brief = node->doc().briefText().toString();
1111
1112 member.nodeType = node->nodeType();
1113 member.access = node->access();
1114 member.status = node->status();
1115 if (const auto deprecation = extractDeprecationData(node)) {
1116 member.deprecatedSince = deprecation->deprecatedSince;
1117 member.typeNoun = deprecation->typeNoun;
1118 member.deprecatedSinceQualified = deprecation->deprecatedSinceQualified;
1119 }
1120
1121 if (node->isFunction()) {
1122 const auto *fn = static_cast<const FunctionNode *>(node);
1123 member.signature = fn->signature(
1125 member.isStatic = fn->isStatic();
1126 member.isConst = fn->isConst();
1127 member.isVirtual = !fn->isNonvirtual();
1128 member.isSignal = fn->isSignal();
1129 member.isSlot = fn->isSlot();
1130 member.overloadNumber = fn->overloadNumber();
1131 member.isPrimaryOverload = fn->isPrimaryOverload();
1132
1133 const Parameters &params = fn->parameters();
1134 for (int i = 0; i < params.count(); ++i) {
1135 IR::ParameterIR param;
1136 param.type = params.at(i).type();
1137 param.name = params.at(i).name();
1138 param.defaultValue = params.at(i).defaultValue();
1139 member.parameters.append(param);
1140 }
1141 } else if (node->isEnumType()) {
1142 const auto *en = static_cast<const EnumNode *>(node);
1143 member.signature = en->isScoped()
1144 ? "enum class %1"_L1.arg(en->name())
1145 : "enum %1"_L1.arg(en->name());
1146
1147 for (const auto &item : en->items()) {
1148 IR::EnumValueIR ev;
1149 ev.name = item.name();
1150 ev.value = item.value();
1151 ev.since = item.since();
1152 member.enumValues.append(ev);
1153 }
1154 } else if (node->isQmlProperty()) {
1155 const auto *qpn = static_cast<const QmlPropertyNode *>(node);
1156 member.signature = qpn->name() + " : "_L1 + qpn->dataType();
1157 member.dataType = qpn->dataType();
1158 member.isAttached = qpn->isAttached();
1159 member.isDefault = qpn->isDefault();
1160 member.isReadOnly = qpn->isReadOnly();
1161 member.isRequired = qpn->isRequired();
1162 } else if (node->isProperty()) {
1163 const auto *pn = static_cast<const PropertyNode *>(node);
1164 member.signature = pn->name() + " : "_L1 + pn->qualifiedDataType();
1165 } else if (node->isTypedef()) {
1166 const auto *td = static_cast<const TypedefNode *>(node);
1167 member.signature = td->associatedEnum()
1168 ? "flags "_L1 + td->name()
1169 : td->name();
1170 } else if (node->nodeType() == NodeType::Variable) {
1171 const auto *vn = static_cast<const VariableNode *>(node);
1172 member.signature = vn->leftType() + vn->name() + vn->rightType();
1173 } else {
1174 member.signature = node->name();
1175 }
1176
1177 if (includeDetail) {
1178 member.anchorId = hrefResolver->anchorForNode(node);
1179 member.synopsis = member.signature;
1180 member.since = node->since();
1181 member.threadSafety = threadSafenessString(node->threadSafeness());
1182
1184 if (!catStr.empty())
1185 member.comparisonCategory = QString::fromStdString(catStr);
1186
1187 if (node->isFunction()) {
1188 const auto *fn = static_cast<const FunctionNode *>(node);
1189 const auto &noexcept_ = fn->getNoexcept();
1190 if (noexcept_) {
1191 member.isNoexcept = true;
1192 member.noexceptNote = *noexcept_;
1193 }
1194 }
1195
1196 const Text &bodyText = node->doc().body();
1197 if (const Atom *firstAtom = bodyText.firstAtom()) {
1198 IR::ContentBuilder contentBuilder(IR::BriefHandling::Include, 0,
1199 diagnosticHandlerFor(node));
1200 member.body = contentBuilder.build(firstAtom);
1201 if (node->isEnumType())
1202 qualifyEnumValueTerms(static_cast<const EnumNode *>(node), member.body);
1203 }
1204
1205 const QList<Text> &alsoTexts = node->doc().alsoList();
1206 for (const Text &alsoText : alsoTexts) {
1207 if (const Atom *firstAtom = alsoText.firstAtom()) {
1208 IR::ContentBuilder contentBuilder(IR::BriefHandling::Include, 0,
1209 diagnosticHandlerFor(node));
1210 QList<IR::ContentBlock> blocks = contentBuilder.build(firstAtom);
1211 member.alsoList.append(blocks);
1212 }
1213 }
1214 }
1215
1216 Section::Style spanStyle = includeDetail ? Section::Details : Section::Summary;
1217 member.signatureSpans = buildSignatureSpans(node, hrefResolver, relative, spanStyle);
1218
1219 return member;
1220}
1221
1222static QList<IR::SignatureSpan> buildTypeSpans(const QString &typeString)
1223{
1224 QList<IR::SignatureSpan> spans;
1225 QString pendingWord;
1226
1227 for (int i = 0; i <= typeString.size(); ++i) {
1228 QChar ch;
1229 if (i != typeString.size())
1230 ch = typeString.at(i);
1231
1232 QChar lower = ch.toLower();
1233 if ((lower >= 'a'_L1 && lower <= 'z'_L1) || ch.digitValue() >= 0
1234 || ch == '_'_L1 || ch == ':'_L1) {
1235 pendingWord += ch;
1236 } else {
1237 if (!pendingWord.isEmpty()) {
1238 bool isProbablyType = (pendingWord != "const"_L1);
1239 IR::SignatureSpan span;
1240 span.role = isProbablyType ? IR::SpanRole::Type : IR::SpanRole::Text;
1241 span.text = pendingWord;
1242 spans.append(span);
1243 }
1244 pendingWord.clear();
1245
1246 if (!ch.isNull()) {
1247 IR::SignatureSpan span;
1249 span.text = QString(ch);
1250 spans.append(span);
1251 }
1252 }
1253 }
1254 return spans;
1255}
1256
1258{
1259 QString extraStr = CodeMarker::extraSynopsis(node, style);
1260 if (extraStr.isEmpty())
1261 return {};
1262
1263 // extraSynopsis may contain <@extref target="...">text</@extref> tags for
1264 // cppreference links. Parse those into ExternalRef spans; everything else
1265 // becomes Extra spans.
1266 static const QRegularExpression extrefRegex(
1267 u"<@extref target=\"([^\"]+)\">([^<]+)</@extref>"_s);
1268
1269 QList<IR::SignatureSpan> spans;
1270 IR::SignatureSpan wrapper;
1271 wrapper.role = IR::SpanRole::Extra;
1272
1273 qsizetype pos = 0;
1274 auto it = extrefRegex.globalMatch(extraStr);
1275 while (it.hasNext()) {
1276 auto match = it.next();
1277 if (match.capturedStart() > pos) {
1278 IR::SignatureSpan textSpan;
1279 textSpan.role = IR::SpanRole::Text;
1280 textSpan.text = extraStr.mid(pos, match.capturedStart() - pos);
1281 wrapper.children.append(textSpan);
1282 }
1283 IR::SignatureSpan ref;
1285 ref.text = match.captured(2);
1286 ref.href = "https://en.cppreference.com/w/cpp/language/"_L1 + match.captured(1);
1287 wrapper.children.append(ref);
1288 pos = match.capturedEnd();
1289 }
1290 if (pos < extraStr.size()) {
1291 IR::SignatureSpan textSpan;
1292 textSpan.role = IR::SpanRole::Text;
1293 textSpan.text = extraStr.mid(pos);
1294 wrapper.children.append(textSpan);
1295 }
1296
1297 if (wrapper.children.isEmpty()) {
1298 wrapper.text = extraStr;
1299 }
1300 spans.append(wrapper);
1301 return spans;
1302}
1303
1305 const HrefResolver *hrefResolver,
1306 const Node *relative)
1307{
1308 if (!templateDecl)
1309 return {};
1310
1311 IR::SignatureSpan declSpan;
1313 declSpan.text = "template"_L1;
1314
1315 IR::SignatureSpan open;
1317 open.text = " <"_L1;
1318 declSpan.children.append(open);
1319
1320 bool first = true;
1321 for (const auto &param : templateDecl->parameters) {
1322 if (param.sfinae_constraint)
1323 continue;
1324 if (!first) {
1325 IR::SignatureSpan comma;
1326 comma.role = IR::SpanRole::Text;
1327 comma.text = ", "_L1;
1328 declSpan.children.append(comma);
1329 }
1330
1331 switch (param.kind) {
1332 case RelaxedTemplateParameter::Kind::TypeTemplateParameter: {
1333 if (param.concept_name) {
1334 const QString fq = QString::fromStdString(*param.concept_name);
1335 const QString unqualified = fq.section("::"_L1, -1);
1336 // Resolve by the fully-qualified name the concept is registered
1337 // under; render the unqualified spelling the author wrote.
1338 const Node *conceptNode = findConceptNodeForRelative(relative, fq);
1339 IR::SignatureSpan link;
1340 link.role = IR::SpanRole::Link;
1341 link.text = unqualified;
1342 link.href = resolveHref(hrefResolver, conceptNode, relative);
1343 declSpan.children.append(link);
1344 } else {
1345 IR::SignatureSpan kw;
1346 kw.role = IR::SpanRole::Text;
1347 kw.text = "typename"_L1;
1348 declSpan.children.append(kw);
1349 }
1350 break;
1351 }
1352 case RelaxedTemplateParameter::Kind::TemplateTemplateParameter: {
1353 IR::SignatureSpan kw;
1354 kw.role = IR::SpanRole::Text;
1355 kw.text = "typename"_L1;
1356 declSpan.children.append(kw);
1357 break;
1358 }
1359 case RelaxedTemplateParameter::Kind::NonTypeTemplateParameter: {
1360 if (!param.valued_declaration.type.empty()) {
1361 auto typeSpans = buildTypeSpans(QString::fromStdString(param.valued_declaration.type));
1362 declSpan.children.append(typeSpans);
1363 }
1364 break;
1365 }
1366 }
1367
1368 if (param.is_parameter_pack) {
1369 IR::SignatureSpan dots;
1370 dots.role = IR::SpanRole::Text;
1371 dots.text = "..."_L1;
1372 declSpan.children.append(dots);
1373 }
1374
1375 if (!param.valued_declaration.name.empty()) {
1376 IR::SignatureSpan space;
1377 space.role = IR::SpanRole::Text;
1378 space.text = " "_L1;
1379 declSpan.children.append(space);
1380
1381 IR::SignatureSpan nameSpan;
1382 nameSpan.role = IR::SpanRole::Parameter;
1383 nameSpan.text = QString::fromStdString(param.valued_declaration.name);
1384 declSpan.children.append(nameSpan);
1385 }
1386
1387 if (!param.valued_declaration.initializer.empty()) {
1388 IR::SignatureSpan eq;
1389 eq.role = IR::SpanRole::Text;
1390 eq.text = " = "_L1;
1391 declSpan.children.append(eq);
1392
1393 if (param.kind == RelaxedTemplateParameter::Kind::TypeTemplateParameter
1394 || param.kind == RelaxedTemplateParameter::Kind::TemplateTemplateParameter) {
1395 auto typeSpans = buildTypeSpans(QString::fromStdString(param.valued_declaration.initializer));
1396 declSpan.children.append(typeSpans);
1397 } else {
1398 IR::SignatureSpan val;
1399 val.role = IR::SpanRole::Text;
1400 val.text = QString::fromStdString(param.valued_declaration.initializer);
1401 declSpan.children.append(val);
1402 }
1403 }
1404
1405 first = false;
1406 }
1407
1408 IR::SignatureSpan close;
1410 close.text = ">"_L1;
1411 declSpan.children.append(close);
1412
1413 if (templateDecl->requires_clause && !templateDecl->requires_clause->empty()) {
1414 const QString reqText = QString::fromStdString(*templateDecl->requires_clause);
1415 QStringList conceptNames;
1416 conceptNames.reserve(int(templateDecl->referenced_concepts.size()));
1417 for (const auto &s : templateDecl->referenced_concepts)
1418 conceptNames.append(QString::fromStdString(s));
1419 appendRequiresSpansWithLinks(declSpan.children, reqText, conceptNames,
1420 hrefResolver, relative);
1421 }
1422
1423 return { declSpan };
1424}
1425
1427 const HrefResolver *hrefResolver,
1428 const Node *relative,
1429 Section::Style style)
1430{
1431 QList<IR::SignatureSpan> spans;
1432
1433 auto appendText = [&spans](const QString &text) {
1434 IR::SignatureSpan span;
1436 span.text = text;
1437 spans.append(span);
1438 };
1439
1440 auto appendName = [&spans, node, hrefResolver, relative](const QString &name) {
1441 IR::SignatureSpan span;
1443 span.text = name;
1444 span.href = resolveHref(hrefResolver, node, relative);
1445 spans.append(span);
1446 };
1447
1448 auto appendTypeSpans = [&spans](const QString &type, bool trailingSpace) {
1449 auto typeSpans = buildTypeSpans(type);
1450 spans.append(typeSpans);
1451 if (trailingSpace && !type.isEmpty()
1452 && !type.endsWith('*'_L1) && !type.endsWith('&'_L1)) {
1453 IR::SignatureSpan space;
1455 space.text = " "_L1;
1456 spans.append(space);
1457 }
1458 };
1459
1460 // Extra qualifiers go first (prepended in CppCodeMarker)
1461 if (style != Section::AllMembers) {
1462 auto extras = buildExtraSpans(node, style);
1463 if (!extras.isEmpty()) {
1464 spans.append(extras);
1465 appendText(" "_L1);
1466 }
1467 }
1468
1469 // Name with parent prefix for Details style
1470 QString nameText = node->name();
1471 bool linkName = (style != Section::Details);
1472
1473 if (style == Section::Details) {
1475 && !node->parent()->name().isEmpty()
1476 && !node->parent()->isHeader() && !node->isProperty() && !node->isQmlNode()) {
1477 nameText = node->parent()->name() + "::"_L1 + nameText;
1478 }
1479 }
1480
1481 switch (node->nodeType()) {
1483 case NodeType::Class:
1484 case NodeType::Struct:
1485 case NodeType::Union:
1486 appendText(Node::nodeTypeString(node->nodeType()) + " "_L1);
1487 if (linkName) {
1488 appendName(nameText);
1489 } else {
1490 IR::SignatureSpan span;
1492 span.text = nameText;
1493 spans.append(span);
1494 }
1495 break;
1496 case NodeType::Function: {
1497 const auto *func = static_cast<const FunctionNode *>(node);
1498
1499 if (style == Section::Details) {
1500 if (auto templateDecl = node->templateDecl()) {
1501 auto tmplSpans = buildTemplateDeclSpans(&*templateDecl, hrefResolver, relative);
1502 spans.append(tmplSpans);
1503 appendText(" "_L1);
1504 }
1505 }
1506
1507 if (style == Section::Summary || style == Section::Accessors) {
1508 if (!func->isNonvirtual())
1509 appendText("virtual "_L1);
1510 }
1511
1512 if (style != Section::AllMembers && !func->returnType().isEmpty())
1513 appendTypeSpans(func->returnTypeString(), true);
1514
1515 if (linkName) {
1516 appendName(nameText);
1517 } else {
1518 IR::SignatureSpan span;
1520 span.text = nameText;
1521 spans.append(span);
1522 }
1523
1524 if (!func->isMacroWithoutParams()) {
1525 appendText("("_L1);
1526 if (!func->parameters().isEmpty()) {
1527 const Parameters &parameters = func->parameters();
1528 for (int i = 0; i < parameters.count(); ++i) {
1529 if (i > 0)
1530 appendText(", "_L1);
1531 const Parameter &param = parameters.at(i);
1532 QString pName = param.name();
1533 QString type = param.type();
1534 QString value = param.defaultValue();
1535 qsizetype insertPos = param.nameInsertionPoint();
1536 if (insertPos >= 0 && style != Section::AllMembers && !pName.isEmpty()) {
1537 appendTypeSpans(type.left(insertPos), false);
1538 IR::SignatureSpan paramSpan;
1539 paramSpan.role = IR::SpanRole::Parameter;
1540 paramSpan.text = pName;
1541 spans.append(paramSpan);
1542 appendTypeSpans(type.mid(insertPos), false);
1543 } else {
1544 bool trailingSpace = style != Section::AllMembers && !pName.isEmpty();
1545 appendTypeSpans(type, trailingSpace);
1546 if (style != Section::AllMembers && !pName.isEmpty()) {
1547 IR::SignatureSpan paramSpan;
1548 paramSpan.role = IR::SpanRole::Parameter;
1549 paramSpan.text = pName;
1550 spans.append(paramSpan);
1551 }
1552 }
1553 if (style != Section::AllMembers && !value.isEmpty())
1554 appendText(" = "_L1 + value);
1555 }
1556 }
1557 appendText(")"_L1);
1558 }
1559
1560 if (func->isConst())
1561 appendText(" const"_L1);
1562
1563 if (style == Section::Summary || style == Section::Accessors) {
1564 if (func->isFinal())
1565 appendText(" final"_L1);
1566 if (func->isOverride())
1567 appendText(" override"_L1);
1568 if (func->isPureVirtual())
1569 appendText(" = 0"_L1);
1570 if (func->isRef())
1571 appendText(" &"_L1);
1572 else if (func->isRefRef())
1573 appendText(" &&"_L1);
1574 } else if (style == Section::AllMembers) {
1575 if (!func->returnType().isEmpty() && func->returnType() != "void"_L1) {
1576 appendText(" : "_L1);
1577 appendTypeSpans(func->returnTypeString(), false);
1578 }
1579 } else {
1580 if (func->isRef())
1581 appendText(" &"_L1);
1582 else if (func->isRefRef())
1583 appendText(" &&"_L1);
1584 if (const auto &req = func->trailingRequiresClause(); req && !req->isEmpty())
1585 appendRequiresSpansWithLinks(spans, *req, func->referencedConcepts(),
1586 hrefResolver, relative);
1587 }
1588 break;
1589 }
1590 case NodeType::Enum: {
1591 const auto *enume = static_cast<const EnumNode *>(node);
1592 appendText("enum"_L1);
1593 if (enume->isScoped())
1594 appendText(" class"_L1);
1595 if (!enume->isAnonymous()) {
1596 appendText(" "_L1);
1597 if (linkName) {
1598 appendName(nameText);
1599 } else {
1600 IR::SignatureSpan span;
1602 span.text = nameText;
1603 spans.append(span);
1604 }
1605 }
1606 if (style == Section::Summary) {
1607 appendText(" { "_L1);
1608 const int MaxEnumValues = 6;
1609 QStringList documentedItems = enume->doc().enumItemNames();
1610 if (documentedItems.isEmpty()) {
1611 const auto &enumItems = enume->items();
1612 for (const auto &item : enumItems)
1613 documentedItems << item.name();
1614 }
1615 const QStringList omitItems = enume->doc().omitEnumItemNames();
1616 for (const auto &item : omitItems)
1617 documentedItems.removeAll(item);
1618
1619 if (documentedItems.size() > MaxEnumValues) {
1620 const QString last = documentedItems.last();
1621 documentedItems = documentedItems.mid(0, MaxEnumValues - 1);
1622 documentedItems += "..."_L1;
1623 documentedItems += last;
1624 }
1625 appendText(documentedItems.join(", "_L1));
1626 if (!documentedItems.isEmpty())
1627 appendText(" "_L1);
1628 appendText("}"_L1);
1629 }
1630 break;
1631 }
1632 case NodeType::TypeAlias: {
1633 if (style == Section::Details) {
1634 if (auto templateDecl = node->templateDecl()) {
1635 auto tmplSpans = buildTemplateDeclSpans(&*templateDecl, hrefResolver, relative);
1636 spans.append(tmplSpans);
1637 appendText(" "_L1);
1638 }
1639 }
1640 if (linkName) {
1641 appendName(nameText);
1642 } else {
1643 IR::SignatureSpan span;
1645 span.text = nameText;
1646 spans.append(span);
1647 }
1648 break;
1649 }
1650 case NodeType::Typedef: {
1651 if (static_cast<const TypedefNode *>(node)->associatedEnum())
1652 appendText("flags "_L1);
1653 if (linkName) {
1654 appendName(nameText);
1655 } else {
1656 IR::SignatureSpan span;
1658 span.text = nameText;
1659 spans.append(span);
1660 }
1661 break;
1662 }
1663 case NodeType::Property: {
1664 const auto *property = static_cast<const PropertyNode *>(node);
1665 if (linkName) {
1666 appendName(nameText);
1667 } else {
1668 IR::SignatureSpan span;
1670 span.text = nameText;
1671 spans.append(span);
1672 }
1673 appendText(" : "_L1);
1674 appendTypeSpans(property->qualifiedDataType(), false);
1675 break;
1676 }
1677 case NodeType::QmlProperty: {
1678 const auto *property = static_cast<const QmlPropertyNode *>(node);
1679 if (linkName) {
1680 appendName(nameText);
1681 } else {
1682 IR::SignatureSpan span;
1684 span.text = nameText;
1685 spans.append(span);
1686 }
1687 appendText(" : "_L1);
1688 appendTypeSpans(property->dataType(), false);
1689 break;
1690 }
1691 case NodeType::Variable: {
1692 const auto *variable = static_cast<const VariableNode *>(node);
1693 if (style == Section::AllMembers) {
1694 if (linkName) {
1695 appendName(nameText);
1696 } else {
1697 IR::SignatureSpan span;
1699 span.text = nameText;
1700 spans.append(span);
1701 }
1702 appendText(" : "_L1);
1703 appendTypeSpans(variable->dataType(), false);
1704 } else {
1705 appendTypeSpans(variable->leftType(), true);
1706 if (linkName) {
1707 appendName(nameText);
1708 } else {
1709 IR::SignatureSpan span;
1711 span.text = nameText;
1712 spans.append(span);
1713 }
1714 appendText(variable->rightType());
1715 }
1716 break;
1717 }
1718 default:
1719 if (linkName) {
1720 appendName(nameText);
1721 } else {
1722 IR::SignatureSpan span;
1724 span.text = nameText;
1725 spans.append(span);
1726 }
1727 break;
1728 }
1729
1730 return spans;
1731}
1732
1733/*!
1734 \internal
1735 Build the signature spans for a QML member.
1736
1737 \a relative is the rendered documentation owner: the QML type whose
1738 page the signature appears on. Inherited members resolve their name
1739 link against that owner rather than the declaring type, because an
1740 internal base type generates no page of its own.
1741*/
1743 const HrefResolver *hrefResolver,
1744 const Node *relative)
1745{
1746 QList<IR::SignatureSpan> spans;
1747
1748 auto appendText = [&spans](const QString &text) {
1749 IR::SignatureSpan span;
1751 span.text = text;
1752 spans.append(span);
1753 };
1754
1755 auto appendTypeSpans = [&spans](const QString &type, bool trailingSpace) {
1756 auto typeSpans = buildTypeSpans(type);
1757 spans.append(typeSpans);
1758 if (trailingSpace && !type.isEmpty()
1759 && !type.endsWith('*'_L1) && !type.endsWith('&'_L1)) {
1760 IR::SignatureSpan space;
1762 space.text = " "_L1;
1763 spans.append(space);
1764 }
1765 };
1766
1767 IR::SignatureSpan nameSpan;
1768 nameSpan.role = IR::SpanRole::Name;
1769 nameSpan.text = node->name();
1770 nameSpan.href = resolveHref(hrefResolver, node, relative);
1771
1772 if (node->isQmlProperty()) {
1773 const auto *pn = static_cast<const QmlPropertyNode *>(node);
1774 spans.append(nameSpan);
1775 appendText(" : "_L1);
1776 appendTypeSpans(pn->dataType(), false);
1777 } else if (node->isFunction(Genus::QML)) {
1778 const auto *func = static_cast<const FunctionNode *>(node);
1779 if (!func->returnType().isEmpty())
1780 appendTypeSpans(func->returnTypeString(), true);
1781 spans.append(nameSpan);
1782 appendText("("_L1);
1783 if (!func->parameters().isEmpty()) {
1784 const Parameters &parameters = func->parameters();
1785 for (int i = 0; i < parameters.count(); ++i) {
1786 if (i > 0)
1787 appendText(", "_L1);
1788 QString pName = parameters.at(i).name();
1789 QString type = parameters.at(i).type();
1790 if (!pName.isEmpty()) {
1791 appendTypeSpans(type, true);
1792 IR::SignatureSpan paramSpan;
1793 paramSpan.role = IR::SpanRole::Parameter;
1794 paramSpan.text = pName;
1795 spans.append(paramSpan);
1796 } else {
1797 IR::SignatureSpan paramSpan;
1798 paramSpan.role = IR::SpanRole::Parameter;
1799 paramSpan.text = type;
1800 spans.append(paramSpan);
1801 }
1802 }
1803 }
1804 appendText(")"_L1);
1805 } else {
1806 spans.append(nameSpan);
1807 }
1808
1809 auto extras = buildExtraSpans(node, Section::Summary);
1810 if (!extras.isEmpty()) {
1811 appendText(" "_L1);
1812 spans.append(extras);
1813 }
1814
1815 return spans;
1816}
1817
1818static QString plainTextFromSpans(const QList<IR::SignatureSpan> &spans)
1819{
1820 QString result;
1821 for (const auto &span : spans)
1822 result += span.plainText();
1823 return result;
1824}
1825
1826/*!
1827 \internal
1828 Build structured signature spans from Node data.
1829
1830 This function produces a QList of SignatureSpan values that carry
1831 semantic roles (Type, Name, Parameter, Extra, and so on) for each
1832 element of a member's synopsis. It parallels what CppCodeMarker's
1833 markedUpSynopsis() and markedUpQmlItem() produce as tagged strings,
1834 but outputs structured IR spans instead.
1835
1836 The \a style parameter controls level of detail: Summary includes
1837 virtual/override qualifiers, Details adds template declarations and
1838 parent prefixes, AllMembers uses a condensed format.
1839*/
1841 const HrefResolver *hrefResolver,
1842 const Node *relative,
1843 Section::Style style)
1844{
1845 if (node->isQmlNode() && !node->isEnumType())
1846 return buildQmlItemSpans(node, hrefResolver, relative);
1847 return buildCppSynopsisSpans(node, hrefResolver, relative, style);
1848}
1849
1850/*!
1851 \internal
1852 Extract a grouped all-members listing for a QML type.
1853
1854 Constructs a Sections object from the QmlTypeNode, extracts
1855 allMembersSection().classNodesList() to group members by
1856 originating QML type, and builds AllMemberEntry items with
1857 QML-specific hints and property group nesting.
1858*/
1859static IR::AllMembersIR extractQmlAllMembersIR(const QmlTypeNode *qcn, const HrefResolver *hrefResolver)
1860{
1861 IR::AllMembersIR result;
1862 result.typeName = qcn->name();
1863 result.typeHref = resolveHref(hrefResolver, qcn, qcn);
1864 result.isQmlType = true;
1865
1866 Sections sections(qcn);
1867 ClassNodesList &groupedMembers = sections.allMembersSection().classNodesList();
1868 if (groupedMembers.isEmpty())
1869 return result;
1870
1871 const InclusionPolicy policy = Config::instance().createInclusionPolicy();
1872
1873 std::function<IR::AllMemberEntry(Node *)> buildEntry = [&](Node *node) -> IR::AllMemberEntry {
1874 IR::AllMemberEntry entry;
1875 entry.signatureSpans = buildQmlItemSpans(node, hrefResolver, qcn);
1876 entry.signature = plainTextFromSpans(entry.signatureSpans);
1877 entry.href = resolveHref(hrefResolver, node, qcn);
1878
1879 if (node->isQmlProperty()) {
1880 auto *qpn = static_cast<QmlPropertyNode *>(node);
1881 QStringList qmlHints = qpn->hints();
1882 if (qpn->isAttached() && !qmlHints.contains("attached"_L1))
1883 qmlHints << "attached"_L1;
1884 for (const auto &h : std::as_const(qmlHints))
1885 entry.hints.append(h);
1886 } else if (node->isAttached()) {
1887 entry.hints.append("attached"_L1);
1888 }
1889
1890 if (node->isPropertyGroup()) {
1891 entry.isPropertyGroup = true;
1892 const auto *scn = static_cast<SharedCommentNode *>(node);
1893 for (auto *child : scn->collective()) {
1894 const NodeContext childContext = child->createContext();
1895 if (!InclusionFilter::isIncluded(policy, childContext))
1896 continue;
1897 entry.children.append(buildEntry(child));
1898 }
1899 }
1900
1901 return entry;
1902 };
1903
1904 auto isVisible = [&policy](Node *node) {
1905 const NodeContext context = node->createContext();
1906 return InclusionFilter::isIncluded(policy, context)
1907 && !(node->isSharingComment() && node->sharedCommentNode()->isPropertyGroup());
1908 };
1909
1910 for (const auto &[originType, nodes] : groupedMembers) {
1911 Q_ASSERT(originType);
1912 if (nodes.isEmpty())
1913 continue;
1914
1915 IR::MemberGroup group;
1916 if (originType != qcn) {
1917 group.typeName = originType->name();
1918 group.typeHref = resolveHref(hrefResolver, originType, qcn);
1919 }
1920
1921 for (auto *node : nodes) {
1922 if (isVisible(node))
1923 group.members.append(buildEntry(node));
1924 }
1925
1926 result.memberGroups.append(group);
1927 }
1928
1929 return result;
1930}
1931
1932/*!
1933 \internal
1934 Extract a flat all-members listing for a C++ class or namespace.
1935
1936 Constructs a Sections object from the aggregate, extracts
1937 allMembersSection().members(), builds AllMemberEntry for each
1938 visible member, and returns an AllMembersIR with isQmlType=false.
1939*/
1940static IR::AllMembersIR extractCppAllMembersIR(const Aggregate *aggregate, const HrefResolver *hrefResolver)
1941{
1942 IR::AllMembersIR result;
1943 result.typeName = aggregate->plainFullName();
1944 result.typeHref = resolveHref(hrefResolver, aggregate, aggregate);
1945 result.isQmlType = false;
1946
1947 Sections sections(aggregate);
1948 const Section &allMembers = sections.allMembersSection();
1949 if (allMembers.isEmpty())
1950 return result;
1951
1952 const InclusionPolicy policy = Config::instance().createInclusionPolicy();
1953
1954 for (const auto *node : allMembers.members()) {
1955 if (node->name().isEmpty())
1956 continue;
1957 const NodeContext context = node->createContext();
1958 if (!InclusionFilter::isIncluded(policy, context))
1959 continue;
1960 if (node->isFunction()
1961 && static_cast<const FunctionNode *>(node)->isIgnored())
1962 continue;
1963
1964 IR::AllMemberEntry entry;
1965 entry.signatureSpans = buildSignatureSpans(node, hrefResolver, aggregate, Section::AllMembers);
1966 entry.signature = plainTextFromSpans(entry.signatureSpans);
1967 entry.href = resolveHref(hrefResolver, node, aggregate);
1968 result.members.append(entry);
1969 }
1970
1971 return result;
1972}
1973
1974/*!
1975 \internal
1976 Extract all-members IR for a page node.
1977
1978 Dispatches to the QML or C++ extraction function based on the page
1979 type. Returns std::nullopt for page types that don't have member
1980 listing pages (generic pages, QML basic types) or when the listing
1981 would be empty.
1982*/
1983std::optional<IR::AllMembersIR> extractAllMembersIR(const PageNode *pn, const HrefResolver *hrefResolver)
1984{
1985 if (pn->isQmlType()) {
1986 const auto *qcn = static_cast<const QmlTypeNode *>(pn);
1987 if (qcn->isQmlBasicType())
1988 return std::nullopt;
1989 auto result = extractQmlAllMembersIR(qcn, hrefResolver);
1990 bool hasMember = false;
1991 for (const auto &group : std::as_const(result.memberGroups)) {
1992 if (!group.members.isEmpty()) {
1993 hasMember = true;
1994 break;
1995 }
1996 }
1997 if (!hasMember)
1998 return std::nullopt;
1999 return result;
2000 }
2001
2002 if (pn->isAggregate() && (pn->isClassNode() || pn->isNamespace())) {
2003 const auto *aggregate = static_cast<const Aggregate *>(pn);
2004 auto result = extractCppAllMembersIR(aggregate, hrefResolver);
2005 if (result.members.isEmpty())
2006 return std::nullopt;
2007 return result;
2008 }
2009
2010 return std::nullopt;
2011}
2012
2013/*!
2014 \internal
2015 Extract navigation metadata from a PageNode.
2016
2017 Reads navigation configuration values (homepage, landingpage,
2018 cppclassespage, qmltypespage) and the node's position in the
2019 documentation tree to produce a breadcrumb chain, sequential
2020 links (previous/next/start), and the configured TOC depth.
2021
2022 The breadcrumb chain follows page-type-specific logic ported
2023 from HtmlGenerator::generateNavigationBar(): static chain
2024 entries for API reference pages (class, QML type), a
2025 navigationParent() walk for generic pages with a 16-item
2026 circular reference cutoff, and a fallback to the first group
2027 page when no navigation parent exists.
2028
2029 Sequential links come from the node's pre-populated link map,
2030 set by QDocDatabase::updateNavigation() before generation runs.
2031*/
2032IR::NavigationData extractNavigationData(const PageNode *pn, const HrefResolver *hrefResolver)
2033{
2034 IR::NavigationData nav;
2035 const Config &config = Config::instance();
2037
2038 const QString navDot = CONFIG_NAVIGATION + Config::dot;
2039 const QString homepage = config.get(navDot + CONFIG_HOMEPAGE).asString();
2040 const QString hometitle = config.get(navDot + CONFIG_HOMETITLE).asString(homepage);
2041 const QString landingpage = config.get(navDot + CONFIG_LANDINGPAGE).asString();
2042 const QString landingtitle = config.get(navDot + CONFIG_LANDINGTITLE).asString(landingpage);
2043 const QString cppclassespage = config.get(navDot + CONFIG_CPPCLASSESPAGE).asString();
2044 const QString cppclassestitle = config.get(navDot + CONFIG_CPPCLASSESTITLE).asString("C++ Classes"_L1);
2045 const QString qmltypespage = config.get(navDot + CONFIG_QMLTYPESPAGE).asString();
2046 const QString qmltypestitle = config.get(navDot + CONFIG_QMLTYPESTITLE).asString("QML Types"_L1);
2047
2048 const QString pageTitle = pn->title();
2049 using CrumbState = IR::NavigationData::CrumbState;
2050
2051 auto resolveCrumb = [&](const QString &targetName)
2052 -> std::pair<QString, CrumbState> {
2053 const Node *target = qdb->findNodeForTarget(targetName, pn);
2054 if (!target)
2055 return {{}, CrumbState::Unresolved};
2056 if (target == pn)
2057 return {{}, CrumbState::Current};
2058 return {resolveHref(hrefResolver, target, pn), CrumbState::Link};
2059 };
2060
2061 if (!homepage.isEmpty()) {
2062 auto [href, state] = resolveCrumb(homepage);
2063 if (state == CrumbState::Current)
2064 return nav;
2065 nav.breadcrumbs.append({hometitle, std::move(href), state});
2066 }
2067
2068 if (!landingpage.isEmpty()) {
2069 auto [href, state] = resolveCrumb(landingpage);
2070 if (state != CrumbState::Current)
2071 nav.breadcrumbs.append({landingtitle, std::move(href), state});
2072 }
2073
2074 if (pn->isClassNode()) {
2075 const bool hasClassesCrumb = !cppclassespage.isEmpty() && !cppclassestitle.isEmpty();
2076 if (hasClassesCrumb) {
2077 auto [href, state] = resolveCrumb(cppclassespage);
2078 nav.breadcrumbs.append({cppclassestitle, std::move(href), state});
2079 }
2080
2081 const auto *moduleNode = qdb->getModuleNode(pn);
2082 QString moduleState;
2083 if (moduleNode && !moduleNode->state().isEmpty())
2084 moduleState = QStringLiteral(" (%1)").arg(moduleNode->state());
2085
2086 if (!pn->physicalModuleName().isEmpty() && moduleNode
2087 && (!moduleState.isEmpty() || moduleNode->title() != cppclassespage)) {
2088 const QString moduleText = hasClassesCrumb || moduleNode->title().isEmpty()
2089 ? moduleNode->name() : moduleNode->title();
2090 nav.breadcrumbs.append({moduleText + moduleState,
2091 resolveHref(hrefResolver, moduleNode, pn),
2092 CrumbState::Link});
2093 }
2094 nav.breadcrumbs.append({pn->name(), {}, CrumbState::Current});
2095 } else if (pn->isQmlType()) {
2096 const bool hasTypesCrumb = !qmltypespage.isEmpty() && !qmltypestitle.isEmpty();
2097 if (hasTypesCrumb) {
2098 auto [href, state] = resolveCrumb(qmltypespage);
2099 nav.breadcrumbs.append({qmltypestitle, std::move(href), state});
2100 }
2101
2102 const auto *moduleNode = qdb->getModuleNode(pn);
2103 QString moduleState;
2104 if (moduleNode && !moduleNode->state().isEmpty())
2105 moduleState = QStringLiteral(" (%1)").arg(moduleNode->state());
2106
2107 if (moduleNode
2108 && (!moduleState.isEmpty() || moduleNode->title() != qmltypespage)) {
2109 const QString moduleText = hasTypesCrumb || moduleNode->title().isEmpty()
2110 ? moduleNode->name() : moduleNode->title();
2111 nav.breadcrumbs.append({moduleText + moduleState,
2112 resolveHref(hrefResolver, moduleNode, pn),
2113 CrumbState::Link});
2114 }
2115 nav.breadcrumbs.append({pn->name(), {}, CrumbState::Current});
2116 } else {
2117 auto currentNode = pn;
2118 std::deque<const Node *> navNodes;
2119 qsizetype navItems = 0;
2120 while (currentNode->navigationParent() && ++navItems < 16) {
2121 if (std::find(navNodes.cbegin(), navNodes.cend(),
2122 currentNode->navigationParent()) == navNodes.cend())
2123 navNodes.push_front(currentNode->navigationParent());
2124 currentNode = currentNode->navigationParent();
2125 }
2126 if (navNodes.empty()) {
2127 const QStringList groups = pn->groupNames();
2128 for (const auto &groupName : groups) {
2129 const auto *groupNode = qdb->findNodeByNameAndType(
2130 QStringList{groupName}, &Node::isGroup);
2131 if (groupNode && !groupNode->title().isEmpty()) {
2132 navNodes.push_front(groupNode);
2133 break;
2134 }
2135 }
2136 }
2137 for (const auto *navNode : navNodes) {
2138 if (navNode->isPageNode())
2139 nav.breadcrumbs.append({navNode->title(),
2140 resolveHref(hrefResolver, navNode, pn),
2141 CrumbState::Link});
2142 }
2143 if (!nav.breadcrumbs.isEmpty())
2144 nav.breadcrumbs.append({pageTitle, {}, CrumbState::Current});
2145 }
2146
2147 const auto &linkMap = pn->links();
2148 if (linkMap.contains(Node::PreviousLink)) {
2149 const auto &linkPair = linkMap[Node::PreviousLink];
2150 const Node *target = qdb->findNodeForTarget(linkPair.first, pn);
2151 QString href;
2152 QString title;
2153 if (target && target != pn) {
2154 href = resolveHref(hrefResolver, target, pn);
2155 title = (linkPair.first == linkPair.second && !target->title().isEmpty())
2156 ? target->title() : linkPair.second;
2157 } else {
2158 href = linkPair.first;
2159 title = linkPair.second;
2160 }
2161 nav.previousLink = IR::NavigationData::LinkEntry{title, href};
2162 }
2163 if (linkMap.contains(Node::NextLink)) {
2164 const auto &linkPair = linkMap[Node::NextLink];
2165 const Node *target = qdb->findNodeForTarget(linkPair.first, pn);
2166 QString href;
2167 QString title;
2168 if (target && target != pn) {
2169 href = resolveHref(hrefResolver, target, pn);
2170 title = (linkPair.first == linkPair.second && !target->title().isEmpty())
2171 ? target->title() : linkPair.second;
2172 } else {
2173 href = linkPair.first;
2174 title = linkPair.second;
2175 }
2176 nav.nextLink = IR::NavigationData::LinkEntry{title, href};
2177 }
2178 if (linkMap.contains(Node::StartLink)) {
2179 const auto &linkPair = linkMap[Node::StartLink];
2180 const Node *target = qdb->findNodeForTarget(linkPair.first, pn);
2181 QString href;
2182 QString title;
2183 if (target && target != pn) {
2184 href = resolveHref(hrefResolver, target, pn);
2185 title = (linkPair.first == linkPair.second && !target->title().isEmpty())
2186 ? target->title() : linkPair.second;
2187 } else {
2188 href = linkPair.first;
2189 title = linkPair.second;
2190 }
2191 nav.startLink = IR::NavigationData::LinkEntry{title, href};
2192 }
2193
2194 const QString formatDot = "HTML"_L1 + Config::dot;
2195 nav.tocDepth = config.get(formatDot + "tocdepth"_L1).asInt();
2196
2197 return nav;
2198}
2199
2200} // namespace NodeExtractor
2201
2202QT_END_NAMESPACE
#define ATOM_LIST_VALUE
Definition atom.h:211
bool hasObsoleteMembers() const
Returns true if this aggregate contains at least one child that is marked obsolete.
The Atom class is the fundamental unit for representing documents internally.
Definition atom.h:19
The ClassNode represents a C++ class.
Definition classnode.h:23
bool isQmlNativeType()
Definition classnode.h:54
A class for holding the members of a collection of doc pages.
NodeMap getMembers(NodeType type) const
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
Text briefText(bool inclusive=false) const
Definition doc.cpp:126
Converts Atom chains to QList<IR::ContentBlock> trees.
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
A PageNode is a Node that generates a documentation page.
Definition pagenode.h:19
const PageNode * navigationParent() const
Definition pagenode.h:47
bool noAutoList() const
Returns the value of the no auto-list flag.
Definition pagenode.h:42
The Parameter class describes one function parameter.
Definition parameter.h:14
This class provides exclusive access to the qdoc database, which consists of a forrest of trees and a...
static QDocDatabase * qdocDB()
Creates the singleton.
const CollectionNode * getModuleNode(const Node *relative)
Returns the collection node representing the module that relative node belongs to,...
const CNMap & groups()
Returns a const reference to the collection of all group nodes in the primary tree.
Status
Specifies the status of the QQmlIncubator.
ClassNode * classNode() const override
If this is a QmlTypeNode, this function returns the pointer to the C++ ClassNode that this QML type r...
Definition qmltypenode.h:27
static void subclasses(const Node *base, NodeList &subs, bool recurse=false)
Loads the list subs with the nodes of all the subclasses of base.
bool isSingleton() const
Definition qmltypenode.h:31
QmlTypeNode * qmlBaseNode() const override
If this Aggregate is a QmlTypeNode, this function returns a pointer to the QmlTypeNode that is its ba...
Definition qmltypenode.h:54
CollectionNode * logicalModule() const override
If this is a QmlTypeNode, a pointer to its QML module is returned, which is a pointer to a Collection...
Definition qmltypenode.h:47
A class for containing the elements of one documentation section.
Definition sections.h:17
ClassNodesList & classNodesList()
Definition sections.h:52
@ Summary
Definition sections.h:19
@ Details
Definition sections.h:19
@ Accessors
Definition sections.h:19
@ AllMembers
Definition sections.h:19
bool isEmpty() const
Definition sections.h:35
A class for creating vectors of collections for documentation.
Definition sections.h:80
SectionVector & summarySections()
Definition sections.h:151
Sections(const Aggregate *aggregate)
This constructor builds the section vectors based on the type of the aggregate node.
Definition sections.cpp:372
bool hasObsoleteMembers(SectionPtrVector *summary_spv, SectionPtrVector *details_spv) const
Returns true if any sections in this object contain obsolete members.
Definition sections.cpp:967
SectionVector & detailsSections()
Definition sections.h:152
Section & allMembersSection()
Definition sections.h:153
Definition text.h:12
const Atom * firstAtom() const
Definition text.h:33
static std::string comparisonCategoryAsString(ComparisonCategory category)
#define CONFIG_CPPCLASSESTITLE
Definition config.h:391
#define CONFIG_HOMETITLE
Definition config.h:411
#define CONFIG_HOMEPAGE
Definition config.h:410
#define CONFIG_URL
Definition config.h:468
#define CONFIG_QMLTYPESPAGE
Definition config.h:474
#define CONFIG_CPPCLASSESPAGE
Definition config.h:390
#define CONFIG_NAVIGATION
Definition config.h:434
#define CONFIG_LANDINGPAGE
Definition config.h:423
#define CONFIG_LANDINGTITLE
Definition config.h:424
#define CONFIG_QMLTYPESTITLE
Definition config.h:475
NodeType
Definition genustypes.h:165
Definition builder.cpp:14
static QList< IR::SignatureSpan > buildTemplateDeclSpans(const RelaxedTemplateDeclaration *templateDecl, const HrefResolver *hrefResolver, const Node *relative)
QList< IR::SectionIR > extractDetailSections(const Aggregate *aggregate, const HrefResolver *hrefResolver, SemanticOrigins *origins)
static void qualifyEnumValueTerms(const EnumNode *en, QList< IR::ContentBlock > &blocks)
static QList< IR::SignatureSpan > buildTypeSpans(const QString &typeString)
IR::PageMetadata extractPageMetadata(const PageNode *pn, const HrefResolver *hrefResolver, SemanticOrigins *origins)
QList< IR::SignatureSpan > buildSignatureSpans(const Node *node, const HrefResolver *hrefResolver, const Node *relative, Section::Style style)
IR::CollectionData extractCollectionData(const CollectionNode *cn, const HrefResolver *hrefResolver)
IR::MemberIR extractMemberIR(const Node *node, const HrefResolver *hrefResolver, const Node *relative, MemberExtractionLevel level)
std::optional< IR::AllMembersIR > extractAllMembersIR(const PageNode *pn, const HrefResolver *hrefResolver)
QList< IR::SectionIR > extractSummarySections(const Aggregate *aggregate, const HrefResolver *hrefResolver)
static QString plainTextFromSpans(const QList< IR::SignatureSpan > &spans)
static IR::AllMembersIR extractQmlAllMembersIR(const QmlTypeNode *qcn, const HrefResolver *hrefResolver)
IR::QmlTypeData extractQmlTypeData(const QmlTypeNode *qcn, const HrefResolver *hrefResolver)
static QList< IR::SignatureSpan > buildQmlItemSpans(const Node *node, const HrefResolver *hrefResolver, const Node *relative)
static QList< IR::SignatureSpan > buildCppSynopsisSpans(const Node *node, const HrefResolver *hrefResolver, const Node *relative, Section::Style style)
static QString threadSafenessString(Node::ThreadSafeness ts)
IR::CppReferenceData extractCppReferenceData(const Aggregate *aggregate, const HrefResolver *hrefResolver)
static IR::AllMembersIR extractCppAllMembersIR(const Aggregate *aggregate, const HrefResolver *hrefResolver)
IR::NavigationData extractNavigationData(const PageNode *pn, const HrefResolver *hrefResolver)
static QList< IR::SignatureSpan > buildExtraSpans(const Node *node, Section::Style style)
QList< Node * > NodeList
Definition node.h:45
QMap< QString, Node * > NodeMap
Definition node.h:48
static QString resolveHref(const HrefResolver *resolver, const Node *target, const Node *relative)
static IR::DiagnosticHandler diagnosticHandlerFor(const Node *node)
MemberExtractionLevel
QList< const Section * > SectionPtrVector
Definition sections.h:77
QList< ClassNodes > ClassNodesList
Definition sections.h:14
@ Deprecated
Definition status.h:12
@ Active
Definition status.h:14
@ Preliminary
Definition status.h:13
A single entry in an all-members listing page.
Definition member.h:127
Intermediate representation of the all-members listing page.
Definition member.h:144
Intermediate representation of a single documentable member.
Definition member.h:53
bool isPrimaryOverload
Definition member.h:70
bool isConst
Definition member.h:75
int overloadNumber
Definition member.h:69
bool isAttached
Definition member.h:81
bool isDefault
Definition member.h:82
bool isSlot
Definition member.h:78
bool isRequired
Definition member.h:84
bool isStatic
Definition member.h:74
bool isNoexcept
Definition member.h:94
bool isSignal
Definition member.h:77
Access access
Definition member.h:61
NodeType nodeType
Definition member.h:60
bool isReadOnly
Definition member.h:83
bool isVirtual
Definition member.h:76
Status status
Definition member.h:62
Intermediate representation of a function parameter.
Definition member.h:21
Represents a single span within a structured signature.
The Node class is the base class for all the nodes in QDoc's parse tree.
const Doc & doc() const
Returns a reference to the node's Doc data member.
Definition node.h:239
bool isQmlNode() const
Returns true if this node's Genus value is QML.
Definition node.h:121
bool isGroup() const
Returns true if the node type is Group.
Definition node.h:105
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
ComparisonCategory comparisonCategory() const
Definition node.h:187
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
Genus genus() const override
Returns this node's Genus.
Definition node.h:85
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
bool isConcept() const
Definition node.h:109
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
bool isProxyNode() const
Returns true if the node type is Proxy.
Definition node.h:115
const std::optional< RelaxedTemplateDeclaration > & templateDecl() const
Definition node.h:247
Access access() const
Returns the node's Access setting, which can be Public, Protected, or Private.
Definition node.h:232
bool isFunction(Genus g=Genus::DontCare) const
Returns true if this is a FunctionNode and its Genus is set to g.
Definition node.h:101
ThreadSafeness threadSafeness() const
Returns the thread safeness value for whatever this node represents.
Definition node.cpp:845
bool isProperty() const
Returns true if the node type is Property.
Definition node.h:114
NodeContext createContext() const
Definition node.cpp:175
bool isModule() const
Returns true if the node type is Module.
Definition node.h:108
ThreadSafeness
An unsigned char that specifies the degree of thread-safeness of the element.
Definition node.h:58
@ ThreadSafe
Definition node.h:62
@ NonReentrant
Definition node.h:60
@ UnspecifiedSafeness
Definition node.h:59
@ Reentrant
Definition node.h:61
@ NextLink
Definition node.h:73
@ StartLink
Definition node.h:73
@ PreviousLink
Definition node.h:73
bool isRelatedNonmember() const
Returns true if this is a related nonmember of something.
Definition node.h:124
virtual bool isClassNode() const
Returns true if this is an instance of ClassNode.
Definition node.h:145
virtual bool isCollectionNode() const
Returns true if this is an instance of CollectionNode.
Definition node.h:146
bool isQmlModule() const
Returns true if the node type is QmlModule.
Definition node.h:120
@ SignatureReturnType
Definition node.h:68
@ SignatureDefaultValues
Definition node.h:67
bool isQmlProperty() const
Returns true if the node type is QmlProperty.
Definition node.h:122
A class for parsing and managing a function parameter list.
Definition main.cpp:28
const Parameter & at(int i) const
Definition parameters.h:36
int count() const
Definition parameters.h:34