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
linkresolver.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
4#ifdef QDOC_TEMPLATE_GENERATOR_ENABLED
5
6#include "linkresolver.h"
7
8#include "genustypes.h"
9#include "hrefresolver.h"
10#include "ir/contentblock.h"
11#include "node.h"
12#include "qdocdatabase.h"
13#include "qdoclogging.h"
14
15using namespace Qt::Literals::StringLiterals;
16
17QT_BEGIN_NAMESPACE
18
19static Genus genusFromString(const QString &s)
20{
21 if (s == "cpp"_L1)
22 return Genus::CPP;
23 if (s == "qml"_L1)
24 return Genus::QML;
25 if (s == "doc"_L1)
26 return Genus::DOC;
27 if (s == "api"_L1)
28 return Genus::API;
29 return Genus::DontCare;
30}
31
32static bool isBrokenAutolink(const IR::InlineContent &inline_)
33{
34 return inline_.type == IR::InlineType::Link
35 && inline_.link.has_value()
36 && inline_.link->state == IR::LinkState::Broken
37 && inline_.link->origin == IR::LinkOrigin::Auto;
38}
39
40/*!
41 Constructs a LinkResolver that uses \a qdb for node lookup,
42 \a hrefResolver for URL computation, and \a config for warning
43 policy.
44*/
45LinkResolver::LinkResolver(QDocDatabase *qdb, const HrefResolver &hrefResolver,
46 const LinkResolverConfig &config)
47 : m_qdb(qdb), m_hrefResolver(hrefResolver), m_config(config)
48{
49}
50
51/*!
52 Walks \a blocks and resolves all unresolved Link inlines in place.
53 The \a relative node provides context for relative URL computation
54 and warning emission.
55
56 This must be called after ContentBuilder produces the block tree
57 and before rendering.
58*/
59void LinkResolver::resolve(QList<IR::ContentBlock> &blocks, const Node *relative)
60{
61 for (auto &block : blocks)
62 resolveBlock(block, relative);
63}
64
65/*!
66 Resolves links within a single \a block by processing its inline
67 content and recursing into child blocks.
68*/
69void LinkResolver::resolveBlock(IR::ContentBlock &block, const Node *relative)
70{
71 resolveInlines(block.inlineContent, relative);
72 for (auto &child : block.children)
73 resolveBlock(child, relative);
74}
75
76/*!
77 Iterates \a inlines and resolves each Link element. Also recurses
78 into children of all inline elements since Link inlines can contain
79 nested formatting (such as Bold or Italic).
80*/
81void LinkResolver::resolveInlines(QList<IR::InlineContent> &inlines, const Node *relative)
82{
83 for (auto &inline_ : inlines) {
84 if (inline_.type == IR::InlineType::Link && !inline_.href.isEmpty())
85 resolveLink(inline_, relative);
86
87 if (isBrokenAutolink(inline_)) {
88 qCDebug(lcQdoc) << "Autolink degraded to text:" << inline_.href;
89 inline_.type = IR::InlineType::Text;
90 inline_.text = inline_.plainText();
91 inline_.href.clear();
92 inline_.children.clear();
93 inline_.link.reset();
94 inline_.attributes = QJsonObject();
95 continue;
96 }
97
98 if (!inline_.children.isEmpty())
99 resolveInlines(inline_.children, relative);
100 }
101}
102
103/*!
104 Core resolution logic for a single Link \a link. Mirrors the behavior
105 of XmlGenerator::getLink() and XmlGenerator::getAutoLink():
106
107 \list
108 \li External URLs (http, https, ftp, file, mailto) are marked without
109 node lookup.
110 \li Node lookup uses genus and module metadata from ContentBuilder
111 when available. Explicit links carry genus scoping (CPP, QML) and
112 module names for tree-scoped search. Autolinks fall back to
113 forest-wide search with DontCare genus.
114 \li Deprecated node links are suppressed when the relative node isn't
115 deprecated.
116 \li HrefResolver returns an HrefResult variant: a URL on success, or
117 an HrefSuppressReason (self-link, policy exclusion, etc.) that
118 maps to LinkState::Suppressed.
119 \li Unresolvable links emit warnings controlled by LinkResolverConfig.
120 \endlist
121
122 The \a relative node provides context for URL computation and
123 warning source location.
124*/
125void LinkResolver::resolveLink(IR::InlineContent &link, const Node *relative)
126{
127 Q_ASSERT(link.link.has_value());
128
129 if (link.link->state != IR::LinkState::Unresolved)
130 return;
131
132 const QString &target = link.href;
133
134 // External URL -- no resolution needed.
135 if (target.startsWith("http:"_L1) || target.startsWith("https:"_L1)
136 || target.startsWith("ftp:"_L1) || target.startsWith("file:"_L1)
137 || target.startsWith("mailto:"_L1)) {
138 link.link->state = IR::LinkState::External;
139 return;
140 }
141
142 // Intra-page anchor link -- references a section heading or member
143 // anchor within the same page. These don't go through node lookup.
144 if (target.startsWith('#'_L1)) {
145 link.link->state = IR::LinkState::Resolved;
146 return;
147 }
148
149 // Use genus and module metadata from ContentBuilder when available.
150 // Explicit links (LinkAtom) carry genus and module; autolinks don't.
151 const Genus genus = genusFromString(
152 link.attributes.value("linkGenus"_L1).toString());
153 const QString &moduleName =
154 link.attributes.value("linkModule"_L1).toString();
155 QString ref;
156 const Node *targetNode =
157 m_qdb->findNodeForTarget(target, relative, genus, moduleName, &ref);
158
159 if (!targetNode) {
160 if (link.link->origin == IR::LinkOrigin::Auto) {
161 if (m_config.autolinkErrors && relative)
162 relative->doc().location().warning(
163 u"Can't autolink to '%1'"_s.arg(target));
164 } else {
165 if (!m_config.noLinkErrors && relative)
166 relative->doc().location().warning(
167 u"Can't link to '%1'"_s.arg(target));
168 }
169
170 link.link->state = IR::LinkState::Broken;
171 link.href.clear();
172 return;
173 }
174
175 // Deprecated node suppression: don't link to deprecated nodes from
176 // non-deprecated content, unless the link originates from within
177 // the deprecated node's own documentation (parent check mirrors
178 // HtmlGenerator::generateBody's inline deprecation logic).
179 if (targetNode->isDeprecated() && relative
180 && relative->parent() != targetNode && !relative->isDeprecated()) {
181 link.href.clear();
182 link.link->state = IR::LinkState::Suppressed;
183 return;
184 }
185
186 // Compute URL via HrefResolver.
187 QString url = targetNode->url();
188 if (url.isNull()) {
189 auto result = m_hrefResolver.hrefForNode(targetNode, relative);
190 if (std::get_if<HrefSuppressReason>(&result)) {
191 link.href.clear();
192 link.link->state = IR::LinkState::Suppressed;
193 return;
194 }
195 url = std::get<QString>(std::move(result));
196 } else if (url.isEmpty()) {
197 link.href.clear();
198 link.link->state = IR::LinkState::Ignored;
199 return;
200 }
201
202 // When the target designates a section title or named anchor within the
203 // page, replace any member anchor the URL may already carry with the one
204 // the lookup computed. Mirrors XmlGenerator::getAutoLink().
205 if (!ref.isEmpty()) {
206 const qsizetype hashIndex = url.lastIndexOf(u'#');
207 if (hashIndex != -1)
208 url.truncate(hashIndex);
209 url += u'#' + ref;
210 }
211
212 link.href = url;
213 link.link->state = IR::LinkState::Resolved;
214}
215
216QT_END_NAMESPACE
217
218#endif // QDOC_TEMPLATE_GENERATOR_ENABLED