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 "location.h"
12#include "node.h"
13#include "qdocdatabase.h"
14#include "qdoclogging.h"
15
16using namespace Qt::Literals::StringLiterals;
17
18QT_BEGIN_NAMESPACE
19
20static Genus genusFromString(const QString &s)
21{
22 if (s == "cpp"_L1)
23 return Genus::CPP;
24 if (s == "qml"_L1)
25 return Genus::QML;
26 if (s == "doc"_L1)
27 return Genus::DOC;
28 if (s == "api"_L1)
29 return Genus::API;
30 return Genus::DontCare;
31}
32
33static bool isBrokenAutolink(const IR::InlineContent &inline_)
34{
35 return inline_.type == IR::InlineType::Link
36 && inline_.link.has_value()
37 && inline_.link->state == IR::LinkState::Broken
38 && inline_.link->origin == IR::LinkOrigin::Auto;
39}
40
41/*!
42 Constructs a LinkResolver that uses \a qdb for node lookup,
43 \a hrefResolver for URL computation, and \a config for warning
44 policy.
45*/
46LinkResolver::LinkResolver(QDocDatabase *qdb, const HrefResolver &hrefResolver,
47 const LinkResolverConfig &config)
48 : m_qdb(qdb), m_hrefResolver(hrefResolver), m_config(config)
49{
50}
51
52/*!
53 Walks \a blocks and resolves all unresolved Link inlines in place.
54
55 \a context.semantic is the node whose documentation the content was
56 authored in: for a page body that is the page node; for a detail
57 member body or see-also list that is the authored member, or the
58 SharedCommentNode for copied shared comment text. Node lookup,
59 deprecated-link suppression, and warning attribution use it. A null
60 semantic falls back to \a context.page, the output page node.
61
62 \a context.page is the output page node that renders the content.
63 URL computation through HrefResolver and cross-module relative URLs
64 use it, so rendered hrefs always stay relative to the rendered page
65 even when the semantic context is a member inside that page.
66
67 This must be called after ContentBuilder produces the block tree
68 and before rendering.
69*/
70void LinkResolver::resolve(QList<IR::ContentBlock> &blocks, Context context)
71{
72 if (!context.semantic)
73 context.semantic = context.page;
74 for (auto &block : blocks)
75 resolveBlock(block, context);
76}
77
78/*!
79 Resolves links within a single \a block by processing its inline
80 content and recursing into child blocks.
81*/
82void LinkResolver::resolveBlock(IR::ContentBlock &block, const Context &context)
83{
84 resolveInlines(block.inlineContent, context);
85 for (auto &child : block.children)
86 resolveBlock(child, context);
87}
88
89/*!
90 Iterates \a inlines and resolves each Link element. Also recurses
91 into children of all inline elements since Link inlines can contain
92 nested formatting (such as Bold or Italic).
93*/
94void LinkResolver::resolveInlines(QList<IR::InlineContent> &inlines, const Context &context)
95{
96 for (auto &inline_ : inlines) {
97 if (inline_.type == IR::InlineType::Link && !inline_.href.isEmpty())
98 resolveLink(inline_, context);
99
100 if (isBrokenAutolink(inline_)) {
101 qCDebug(lcQdoc) << "Autolink degraded to text:" << inline_.href;
102 inline_.type = IR::InlineType::Text;
103 inline_.text = inline_.plainText();
104 inline_.href.clear();
105 inline_.children.clear();
106 inline_.link.reset();
107 inline_.attributes = QJsonObject();
108 continue;
109 }
110
111 if (!inline_.children.isEmpty())
112 resolveInlines(inline_.children, context);
113 }
114}
115
116/*!
117 Core resolution logic for a single Link \a link. Mirrors the behavior
118 of XmlGenerator::getLink() and XmlGenerator::getAutoLink():
119
120 \list
121 \li External URLs (http, https, ftp, file, mailto) are marked without
122 node lookup.
123 \li Node lookup uses the \a context.semantic node as the search
124 context, and genus and module metadata from ContentBuilder when
125 available. Explicit links carry genus scoping (CPP, QML) and
126 module names for tree-scoped search. Autolinks fall back to
127 forest-wide search with DontCare genus.
128 \li Deprecated targets suppress autolinks when the semantic node isn't
129 deprecated; explicit \\l and \\sa links keep the author's reference.
130 \li HrefResolver computes the URL relative to the \a context.page node
131 and returns an HrefResult variant: a URL on success, or an
132 HrefSuppressReason (self-link, policy exclusion, etc.) that maps
133 to LinkState::Suppressed.
134 \li Unresolvable links emit warnings controlled by LinkResolverConfig.
135 \endlist
136
137 The \a context.semantic node provides the context for node lookup,
138 deprecation behavior, and warning source location; the \a context.page
139 node provides the context for URL computation.
140*/
141void LinkResolver::resolveLink(IR::InlineContent &link, const Context &context)
142{
143 Q_ASSERT(link.link.has_value());
144
145 if (link.link->state != IR::LinkState::Unresolved)
146 return;
147
148 const QString &target = link.href;
149
150 // External URL -- no resolution needed.
151 if (target.startsWith("http:"_L1) || target.startsWith("https:"_L1)
152 || target.startsWith("ftp:"_L1) || target.startsWith("file:"_L1)
153 || target.startsWith("mailto:"_L1)) {
154 link.link->state = IR::LinkState::External;
155 return;
156 }
157
158 // Intra-page anchor link -- references a section heading or member
159 // anchor within the same page. These don't go through node lookup.
160 if (target.startsWith('#'_L1)) {
161 link.link->state = IR::LinkState::Resolved;
162 return;
163 }
164
165 // Use genus and module metadata from ContentBuilder when available.
166 // Explicit links (LinkAtom) carry genus and module; autolinks don't.
167 const Genus genus = genusFromString(
168 link.attributes.value("linkGenus"_L1).toString());
169 const QString &moduleName =
170 link.attributes.value("linkModule"_L1).toString();
171 const int searchFlags = link.attributes.value("linkFlags"_L1).toInt();
172 QString ref;
173 const LinkReferenceOrigin linkOrigin =
174 link.link->origin == IR::LinkOrigin::Explicit
175 ? LinkReferenceOrigin::Explicit
176 : LinkReferenceOrigin::Auto;
177 const Node *targetNode = m_qdb->findNodeForTarget(
178 target, context.semantic, genus, moduleName, &ref, linkOrigin, searchFlags);
179
180 if (!targetNode) {
181 const auto reportAt = [&](const QString &message) {
182 if (link.link->sourceLocation) {
183 Location loc(link.link->sourceLocation->filePath);
184 loc.setLineNo(link.link->sourceLocation->lineNo);
185 loc.warning(message);
186 } else if (context.semantic) {
187 context.semantic->doc().location().warning(message);
188 }
189 };
190 if (link.link->origin == IR::LinkOrigin::Auto) {
191 if (m_config.autolinkErrors)
192 reportAt(u"Can't autolink to '%1'"_s.arg(target));
193 } else {
194 if (!m_config.noLinkErrors)
195 reportAt(u"Can't link to '%1'"_s.arg(target));
196 }
197
198 link.link->state = IR::LinkState::Broken;
199 link.href.clear();
200 return;
201 }
202
203 // Deprecated node suppression: autolinks to deprecated nodes are
204 // suppressed when the semantic node isn't deprecated, unless the link
205 // originates from within the deprecated node's own documentation
206 if (link.link->origin == IR::LinkOrigin::Auto
207 && targetNode->isDeprecated() && context.semantic
208 && context.semantic->parent() != targetNode && !context.semantic->isDeprecated()) {
209 link.href.clear();
210 link.link->state = IR::LinkState::Suppressed;
211 return;
212 }
213
214 // Compute URL via HrefResolver relative to the rendered page.
215 // Cross-tree references reach LinkResolver carrying a URL baked at
216 // .index-load time using Generator::s_outSubdir, which is only
217 // updated by Generator-derived pipelines (HTML, DocBook).
218 // TemplateGenerator doesn't touch that static, so its baked URLs have
219 // the wrong relative depth for its layout. Route those references
220 // through hrefForNode's strip-and-recompute path so the emitted href
221 // matches the current OutputContext.
222 QString url = targetNode->url();
223 const bool isCrossTreeIndexLoaded = context.page && !targetNode->isExternalPage()
224 && (targetNode->isIndexNode() || targetNode->tree() != context.page->tree());
225 if (url.isNull() || (isCrossTreeIndexLoaded && !url.isEmpty())) {
226 auto result = m_hrefResolver.hrefForNode(targetNode, context.page);
227 if (std::get_if<HrefSuppressReason>(&result)) {
228 link.href.clear();
229 link.link->state = IR::LinkState::Suppressed;
230 return;
231 }
232 url = std::get<QString>(std::move(result));
233 } else if (url.isEmpty()) {
234 link.href.clear();
235 link.link->state = IR::LinkState::Ignored;
236 return;
237 }
238
239 // When the target designates a section title or named anchor within the
240 // page, replace any member anchor the URL may already carry with the one
241 // the lookup computed. Mirrors XmlGenerator::getAutoLink().
242 if (!ref.isEmpty()) {
243 const qsizetype hashIndex = url.lastIndexOf(u'#');
244 if (hashIndex != -1)
245 url.truncate(hashIndex);
246 url += u'#' + ref;
247 }
248
249 link.href = url;
250 link.link->state = IR::LinkState::Resolved;
251}
252
253QT_END_NAMESPACE
254
255#endif // QDOC_TEMPLATE_GENERATOR_ENABLED