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 "qmltypenode.h"
11#include "tree.h"
12#include "utilities.h"
13
14#include <QtCore/qfileinfo.h>
15
16QT_BEGIN_NAMESPACE
17
18/*!
19 \struct HrefResolverConfig
20 \internal
21 \brief Configuration for HrefResolver URL computation.
22
23 The \c outputPrefixFn and \c outputSuffixFn callbacks compute the
24 output prefix and suffix for a given node. These correspond to
25 Generator::outputPrefix() and Generator::outputSuffix() in the
26 legacy path. Both should be set; an empty callback produces an
27 empty prefix or suffix.
28
29 The \c cleanRefFn callback sanitizes anchor references. It
30 corresponds to Generator::cleanRef() in the legacy path.
31
32 The \c qmlTypeContextFn callback returns the current QML type
33 being documented, enabling property inheritance resolution for
34 abstract QML types. An empty callback disables this resolution.
35*/
36
37/*!
38 \class HrefResolver
39 \internal
40 \brief Computes URLs for documentation nodes without Generator dependencies.
41
42 Not thread-safe: the mutable fileBase cache is written from const
43 methods. Create one instance per thread or synchronize externally.
44*/
45
46HrefResolver::HrefResolver(const HrefResolverConfig &config)
47 : m_config(config)
48{
49}
50
51/*!
52 Computes the filename stem for \a node without mutating the node.
53 Results are cached in m_fileBaseCache to avoid recomputation.
54
55 This mirrors Generator::fileBase() but uses an external QHash cache
56 instead of const_cast on the Node.
57*/
58QString HrefResolver::fileBase(const Node *node) const
59{
60 if (!node->isPageNode() && !node->isCollectionNode()) {
61 node = node->parent();
62 if (!node)
63 return {};
64 }
65
66 if (node->hasFileNameBase())
67 return node->fileNameBase();
68
69 auto it = m_fileBaseCache.constFind(node);
70 if (it != m_fileBaseCache.constEnd())
71 return it.value();
72
73 QString result = Utilities::computeFileBase(
74 node, m_config.project,
75 m_config.outputPrefixFn,
76 m_config.outputSuffixFn);
77
78 m_fileBaseCache.insert(node, result);
79 return result;
80}
81
82/*!
83 Computes the output filename for \a node. Returns the node's
84 explicit URL if one is set, otherwise constructs a filename
85 from fileBase() and the configured file extension.
86
87 This mirrors Generator::fileName() without depending on a
88 Generator instance.
89*/
90QString HrefResolver::fileName(const Node *node) const
91{
92 if (!node->url().isEmpty())
93 return node->url();
94
95 const auto base = fileBase(node);
96 if (base.isEmpty())
97 return {};
98
99 if (node->isTextPageNode() && !node->isCollectionNode()) {
100 QFileInfo originalName(node->name());
101 QString suffix = originalName.suffix();
102 if (!suffix.isEmpty() && suffix != "html"_L1)
103 return base + '.'_L1 + suffix;
104 }
105
106 return base + '.'_L1 + m_config.fileExtension;
107}
108
109/*!
110 Computes the anchor fragment for \a node based on its type.
111 Returns an empty string for page-level nodes that don't need
112 anchors.
113
114 Delegates to the free function computeAnchorId() for node-type
115 dispatch, then applies the configured cleanRefFn for sanitization.
116 This keeps the shared anchor logic generator-agnostic while
117 allowing each caller to control cleanup policy.
118*/
119QString HrefResolver::anchorForNode(const Node *node) const
120{
121 const QString ref = computeAnchorId(node);
122 if (ref.isEmpty())
123 return ref;
124 return m_config.cleanRefFn ? m_config.cleanRefFn(ref) : ref;
125}
126
127/*!
128 Computes a relative URL for \a node, relative to \a relative.
129 Returns the URL as a QString on success, or an HrefSuppressReason
130 explaining why linking should be suppressed.
131
132 This consolidates logic from XmlGenerator::linkForNode() without
133 depending on a Generator instance. Cross-module prefixing is
134 applied when useOutputSubdirs is enabled and the node lives in
135 a different tree than the relative node.
136*/
137HrefResult HrefResolver::hrefForNode(const Node *node, const Node *relative) const
138{
139 if (node == nullptr)
140 return HrefSuppressReason::NullNode;
141 if (!node->url().isEmpty())
142 return node->url();
143
144 QString fn = fileName(node);
145 if (fn.isEmpty())
146 return HrefSuppressReason::NoFileBase;
147
148 const NodeContext context = node->createContext();
149 if (!InclusionFilter::isIncluded(m_config.inclusionPolicy, context))
150 return HrefSuppressReason::ExcludedByPolicy;
151
152 if (node->parent() && node->parent()->isQmlType() && node->parent()->isAbstract()) {
153 const QmlTypeNode *qmlContext = m_config.qmlTypeContextFn
154 ? m_config.qmlTypeContextFn() : nullptr;
155 if (qmlContext) {
156 if (qmlContext->inherits(node->parent())) {
157 fn = fileName(qmlContext);
158 } else if (node->parent()->isInternal() && !m_config.noLinkErrors) {
159 node->doc().location().warning(
160 u"Cannot link to property in internal type '%1'"_s
161 .arg(node->parent()->name()));
162 return HrefSuppressReason::InternalAbstractQml;
163 }
164 }
165 }
166
167 QString link = fn;
168
169 if (!node->isPageNode() || node->isPropertyGroup()) {
170 QString ref = anchorForNode(node);
171 if (relative && fn == fileName(relative) && ref == anchorForNode(relative))
172 return HrefSuppressReason::SameFileAnchor;
173
174 link += '#'_L1;
175 link += ref;
176 }
177
178 if (relative && (node != relative)) {
179 if (m_config.useOutputSubdirs && !node->isExternalPage()
180 && (node->isIndexNode() || node->tree() != relative->tree()))
181 link.prepend("../%1/"_L1.arg(node->tree()->physicalModuleName()));
182 }
183
184 return link;
185}
186
187QT_END_NAMESPACE
188
189#endif // QDOC_TEMPLATE_GENERATOR_ENABLED