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 QString ref;
172 const Node *targetNode =
173 m_qdb->findNodeForTarget(target, context.semantic, genus, moduleName, &ref);
174
175 if (!targetNode) {
176 const auto reportAt = [&](const QString &message) {
177 if (link.link->sourceLocation) {
178 Location loc(link.link->sourceLocation->filePath);
179 loc.setLineNo(link.link->sourceLocation->lineNo);
180 loc.warning(message);
181 } else if (context.semantic) {
182 context.semantic->doc().location().warning(message);
183 }
184 };
185 if (link.link->origin == IR::LinkOrigin::Auto) {
186 if (m_config.autolinkErrors)
187 reportAt(u"Can't autolink to '%1'"_s.arg(target));
188 } else {
189 if (!m_config.noLinkErrors)
190 reportAt(u"Can't link to '%1'"_s.arg(target));
191 }
192
193 link.link->state = IR::LinkState::Broken;
194 link.href.clear();
195 return;
196 }
197
198 // Deprecated node suppression: autolinks to deprecated nodes are
199 // suppressed when the semantic node isn't deprecated, unless the link
200 // originates from within the deprecated node's own documentation
201 if (link.link->origin == IR::LinkOrigin::Auto
202 && targetNode->isDeprecated() && context.semantic
203 && context.semantic->parent() != targetNode && !context.semantic->isDeprecated()) {
204 link.href.clear();
205 link.link->state = IR::LinkState::Suppressed;
206 return;
207 }
208
209 // Compute URL via HrefResolver relative to the rendered page.
210 // Cross-tree references reach LinkResolver carrying a URL baked at
211 // .index-load time using Generator::s_outSubdir, which is only
212 // updated by Generator-derived pipelines (HTML, DocBook).
213 // TemplateGenerator doesn't touch that static, so its baked URLs have
214 // the wrong relative depth for its layout. Route those references
215 // through hrefForNode's strip-and-recompute path so the emitted href
216 // matches the current OutputContext.
217 QString url = targetNode->url();
218 const bool isCrossTreeIndexLoaded = context.page && !targetNode->isExternalPage()
219 && (targetNode->isIndexNode() || targetNode->tree() != context.page->tree());
220 if (url.isNull() || (isCrossTreeIndexLoaded && !url.isEmpty())) {
221 auto result = m_hrefResolver.hrefForNode(targetNode, context.page);
222 if (std::get_if<HrefSuppressReason>(&result)) {
223 link.href.clear();
224 link.link->state = IR::LinkState::Suppressed;
225 return;
226 }
227 url = std::get<QString>(std::move(result));
228 } else if (url.isEmpty()) {
229 link.href.clear();
230 link.link->state = IR::LinkState::Ignored;
231 return;
232 }
233
234 // When the target designates a section title or named anchor within the
235 // page, replace any member anchor the URL may already carry with the one
236 // the lookup computed. Mirrors XmlGenerator::getAutoLink().
237 if (!ref.isEmpty()) {
238 const qsizetype hashIndex = url.lastIndexOf(u'#');
239 if (hashIndex != -1)
240 url.truncate(hashIndex);
241 url += u'#' + ref;
242 }
243
244 link.href = url;
245 link.link->state = IR::LinkState::Resolved;
246}
247
248QT_END_NAMESPACE
249
250#endif // QDOC_TEMPLATE_GENERATOR_ENABLED