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
hrefresolver.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#include "hrefresolver.h"
5
6#ifdef QDOC_TEMPLATE_GENERATOR_ENABLED
7
8#include "anchorid.h"
9#include "inclusionfilter.h"
10#include "outputcontext.h"
11#include "qmltypenode.h"
12#include "tree.h"
13#include "utilities.h"
14
15#include <QtCore/qdir.h>
16#include <QtCore/qfileinfo.h>
17
18QT_BEGIN_NAMESPACE
19
20/*!
21 \struct HrefResolverConfig
22 \internal
23 \brief Configuration for HrefResolver URL computation.
24
25 The non-owning \c context pointer is the single source of truth for
26 output layout (project name, file extension, per-genus prefixes and
27 suffixes, subdir usage, diagnostic gating). It must outlive the
28 HrefResolver.
29
30 The \c cleanRefFn callback sanitizes anchor references. It
31 corresponds to Generator::cleanRef() in the legacy path.
32
33 The \c qmlTypeContextFn callback returns the current QML type
34 being documented, enabling property inheritance resolution for
35 abstract QML types. An empty callback disables this resolution.
36*/
37
38/*!
39 \class HrefResolver
40 \internal
41 \brief Computes URLs for documentation nodes without Generator dependencies.
42
43 Not thread-safe: the mutable fileBase cache is written from const
44 methods. Create one instance per thread or synchronize externally.
45*/
46
47HrefResolver::HrefResolver(const HrefResolverConfig &config)
48 : m_config(config)
49{
50}
51
52/*!
53 Computes the filename stem for \a node without mutating the node.
54 Results are cached in m_fileBaseCache to avoid recomputation.
55
56 This mirrors Generator::fileBase() but uses an external QHash cache
57 instead of const_cast on the Node.
58*/
59QString HrefResolver::fileBase(const Node *node) const
60{
61 if (!node->isPageNode() && !node->isCollectionNode()) {
62 node = node->parent();
63 if (!node)
64 return {};
65 }
66
67 if (node->hasFileNameBase())
68 return node->fileNameBase();
69
70 auto it = m_fileBaseCache.constFind(node);
71 if (it != m_fileBaseCache.constEnd())
72 return it.value();
73
74 const OutputContext *ctx = m_config.context;
75 QString result = Utilities::computeFileBase(
76 node, ctx->project,
77 [ctx](const Node *n) { return ctx->outputPrefix(n->genus()); },
78 [ctx](const Node *n) { return ctx->outputSuffix(n->genus()); });
79
80 m_fileBaseCache.insert(node, result);
81 return result;
82}
83
84/*!
85 Computes the output filename for \a node. Returns the node's
86 explicit URL if one is set, otherwise constructs a filename
87 from fileBase() and the configured file extension.
88
89 This mirrors Generator::fileName() without depending on a
90 Generator instance.
91*/
92QString HrefResolver::fileName(const Node *node) const
93{
94 if (!node->url().isEmpty())
95 return node->url();
96
97 const auto base = fileBase(node);
98 if (base.isEmpty())
99 return {};
100
101 if (node->isTextPageNode() && !node->isCollectionNode()) {
102 QFileInfo originalName(node->name());
103 QString suffix = originalName.suffix();
104 if (!suffix.isEmpty() && suffix != "html"_L1)
105 return base + '.'_L1 + suffix;
106 }
107
108 return base + '.'_L1 + m_config.context->fileExtension;
109}
110
111/*!
112 Computes the anchor fragment for \a node based on its type.
113 Returns an empty string for page-level nodes that don't need
114 anchors.
115
116 Delegates to the free function computeAnchorId() for node-type
117 dispatch, then applies the configured cleanRefFn for sanitization.
118 This keeps the shared anchor logic generator-agnostic while
119 allowing each caller to control cleanup policy.
120*/
121QString HrefResolver::anchorForNode(const Node *node) const
122{
123 const QString ref = computeAnchorId(node);
124 if (ref.isEmpty())
125 return ref;
126 return m_config.cleanRefFn ? m_config.cleanRefFn(ref) : ref;
127}
128
129/*!
130 Computes a relative URL for \a node, relative to \a relative.
131 Returns the URL as a QString on success, or an HrefSuppressReason
132 explaining why linking should be suppressed.
133
134 External URLs (carrying a scheme such as \c http://) pass through
135 unchanged. Every other reference — including URLs loaded from a
136 dependency's \c .index file — is reduced to a bare filename stem
137 and then prefixed with whatever relative path reaches the target
138 module's output directory from the current page's output
139 directory. The prefix is derived from OutputContext and the two
140 nodes' trees, not from a hardcoded \c ../<module>/ pattern, so it
141 comes out correct regardless of whether the qdocconf uses
142 per-module subdirs, a format subdirectory, or both.
143*/
144HrefResult HrefResolver::hrefForNode(const Node *node, const Node *relative) const
145{
146 if (node == nullptr)
147 return HrefSuppressReason::NullNode;
148
149 const bool hasExplicitUrl = !node->url().isEmpty();
150 if (hasExplicitUrl && node->url().contains("://"_L1))
151 return node->url();
152
153 // An explicit URL arrives with a directory prefix baked in by the
154 // reader that produced it (qdocindexfiles prepends ../<module>/).
155 // That prefix encodes one specific layout's depth assumption and
156 // is wrong for any other layout, so discard it and recompute from
157 // the current output paths below.
158 QString fn = hasExplicitUrl
159 ? node->url().section('/'_L1, -1)
160 : fileName(node);
161 if (fn.isEmpty())
162 return HrefSuppressReason::NoFileBase;
163
164 const NodeContext context = node->createContext();
165 if (!InclusionFilter::isIncluded(m_config.inclusionPolicy, context))
166 return HrefSuppressReason::ExcludedByPolicy;
167
168 if (!hasExplicitUrl && node->parent() && node->parent()->isQmlType()
169 && node->parent()->isAbstract()) {
170 const QmlTypeNode *qmlContext = m_config.qmlTypeContextFn
171 ? m_config.qmlTypeContextFn() : nullptr;
172 if (qmlContext) {
173 if (qmlContext->inherits(node->parent())) {
174 fn = fileName(qmlContext);
175 } else if (node->parent()->isInternal() && !m_config.context->noLinkErrors) {
176 node->doc().location().warning(
177 u"Cannot link to property in internal type '%1'"_s
178 .arg(node->parent()->name()));
179 return HrefSuppressReason::InternalAbstractQml;
180 }
181 }
182 }
183
184 QString link = fn;
185
186 if (!node->isPageNode() || node->isPropertyGroup()) {
187 QString ref = anchorForNode(node);
188 if (relative && fn == fileName(relative) && ref == anchorForNode(relative))
189 return HrefSuppressReason::SameFileAnchor;
190
191 link += '#'_L1;
192 link += ref;
193 }
194
195 if (relative && (node != relative) && !node->isExternalPage()
196 && (node->isIndexNode() || node->tree() != relative->tree())) {
197 link.prepend(crossModulePrefix(node, relative));
198 }
199
200 return link;
201}
202
203/*!
204 \internal
205 Returns a relative-path prefix (ending in \c /) that reaches \a
206 target's output directory from \a source's output directory, or
207 an empty string when the two share a directory.
208
209 The current page's output directory is the OutputContext's \c
210 outputDir. The target's output directory is derived by locating
211 the current module's name as a segment in that path and swapping
212 in the target module's name. If the current module's name isn't
213 present — a flat layout where multiple modules render into the
214 same directory — source and target share a directory and no
215 prefix is needed.
216*/
217QString HrefResolver::crossModulePrefix(const Node *target, const Node *source) const
218{
219 const QString &sourceDir = m_config.context->outputDir.path();
220 const QString currentSegment = '/'_L1 + source->tree()->physicalModuleName() + '/'_L1;
221 const qsizetype moduleIndex = sourceDir.indexOf(currentSegment);
222 if (moduleIndex < 0)
223 return {};
224
225 const QString targetSegment = '/'_L1 + target->tree()->physicalModuleName() + '/'_L1;
226 QString targetDir = sourceDir;
227 targetDir.replace(moduleIndex, currentSegment.size(), targetSegment);
228
229 const QString relPath = QDir(sourceDir).relativeFilePath(targetDir);
230 if (relPath.isEmpty() || relPath == "."_L1)
231 return {};
232 return relPath + '/'_L1;
233}
234
235QT_END_NAMESPACE
236
237#endif // QDOC_TEMPLATE_GENERATOR_ENABLED