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
listexpander.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 "listexpander.h"
7
8#include "contentblock.h"
9#include "listplaceholder.h"
10
11#include <optional>
12#include <utility>
13
14QT_BEGIN_NAMESPACE
15
16using namespace Qt::Literals::StringLiterals;
17
18/*!
19 \class IR::ListExpander
20 \internal
21 \brief Post-IR pass that replaces ListPlaceholder blocks with
22 populated Catalog subtrees.
23
24 ListExpander walks a block tree in place and, for every
25 ListPlaceholder encountered, reads its variant attribute and
26 produces a Catalog wrapper whose children reuse the existing
27 Table, SectionHeading, List, ListItem, and Link block types.
28 Entry data comes through the ListExpanderCallbacks struct, which
29 the driver populates with lambdas bound to a CatalogEntrySource.
30 When a callback returns an empty set the placeholder is removed
31 and \c{ListExpanderCallbacks::onEmpty} is invoked so the driver
32 can warn the author with whatever attribution it has — the
33 expander itself logs nothing, keeping QDocLib free of
34 driver-layer logging categories.
35
36 The expander is QDocLib-pure: it has no Node or QDocDatabase
37 dependencies and receives the relative-node pointer purely as
38 opaque state forwarded to the callbacks.
39
40 \sa CatalogEntry, CatalogEntryGroup, ListExpanderCallbacks,
41 ListPlaceholderVariant
42*/
43
44/*!
45 \struct IR::ListExpanderCallbacks
46 \internal
47 \brief Callback bundle that injects entry data and warning
48 handling into ListExpander.
49
50 Each data callback is the source for one ListPlaceholder variant;
51 \c{onEmpty} is the warning hook invoked when an enumeration
52 yields no entries. The driver-wiring commit binds these to
53 CatalogEntrySource methods and a \c{qCWarning(lcQdoc)} call; the
54 QDocLib expander itself has no knowledge of the bound types
55 beyond the value-semantics CatalogEntry and CatalogEntryGroup it
56 consumes.
57*/
58
59namespace IR {
60
61namespace {
62
63ContentBlock makeSectionHeading(int level, const QString &text,
64 const QString &anchorId)
65{
66 ContentBlock heading;
67 heading.type = BlockType::SectionHeading;
68 heading.attributes["level"_L1] = level;
69 if (!anchorId.isEmpty())
70 heading.attributes["id"_L1] = anchorId;
71 InlineContent textInline;
72 textInline.type = InlineType::Text;
73 textInline.text = text;
74 heading.inlineContent.append(textInline);
75 return heading;
76}
77
78InlineContent makeLink(const QString &name, const QString &href)
79{
80 InlineContent link;
81 link.type = InlineType::Link;
82 link.href = href;
83 // The catalog entry source pre-resolves hrefs through HrefResolver,
84 // so the link arrives at the resolver pipeline already complete.
85 // Mark it Resolved so LinkResolver's body walk skips re-resolution
86 // (and skips its assert that every Link inline carries LinkData).
87 link.link = InlineContent::LinkData{ LinkOrigin::Explicit, LinkState::Resolved };
88 InlineContent nameInline;
89 nameInline.type = InlineType::Text;
90 nameInline.text = name;
91 link.children.append(nameInline);
92 return link;
93}
94
95InlineContent makeText(const QString &text)
96{
97 InlineContent t;
98 t.type = InlineType::Text;
99 t.text = text;
100 return t;
101}
102
103ContentBlock makeCell(QList<InlineContent> inlines)
104{
105 ContentBlock cell;
106 cell.type = BlockType::TableCell;
107 cell.inlineContent = std::move(inlines);
108 return cell;
109}
110
111ContentBlock makeRow(QList<ContentBlock> cells)
112{
113 ContentBlock row;
114 row.type = BlockType::TableRow;
115 row.children = std::move(cells);
116 return row;
117}
118
119ContentBlock makeAnnotatedTable(const QList<CatalogEntry> &entries)
120{
121 ContentBlock table;
122 table.type = BlockType::Table;
123 table.attributes["style"_L1] = u"annotated"_s;
124
125 for (const CatalogEntry &e : entries) {
126 QList<InlineContent> nameInlines;
127 if (!e.href.isEmpty())
128 nameInlines.append(makeLink(e.name, e.href));
129 else
130 nameInlines.append(makeText(e.name));
131
132 QList<InlineContent> briefInlines;
133 briefInlines.append(makeText(e.brief));
134
135 QList<ContentBlock> cells;
136 cells.append(makeCell(std::move(nameInlines)));
137 cells.append(makeCell(std::move(briefInlines)));
138
139 ContentBlock row = makeRow(std::move(cells));
140 // Surface the per-entry metadata CatalogEntrySource extracts.
141 // Templates can render \since stamps next to the name and
142 // strike-through or annotate deprecated rows. Empty since and
143 // non-deprecated rows omit the keys to keep JSON output lean.
144 if (!e.since.isEmpty())
145 row.attributes["since"_L1] = e.since;
146 if (e.isDeprecated)
147 row.attributes["deprecated"_L1] = true;
148 table.children.append(std::move(row));
149 }
150 return table;
151}
152
153// Emits a flat List of class entries. The legacy compact-classes
154// page bucketed entries into 37 alphabetical paragraphs (0-9, A-Z,
155// _), but that layout is presentation, not structure: the project's
156// IR-philosophy decision (see project decisions, 2026-02-20) keeps
157// format details in templates, not compiled C++. The
158// \c{compact-classes} template partial is responsible for any
159// bucketing it wants to render — this function emits the raw,
160// alphabetized data only.
161ContentBlock makeCompactList(const QList<CatalogEntry> &entries)
162{
163 ContentBlock list;
164 list.type = BlockType::List;
165 list.attributes["style"_L1] = u"bullet"_s;
166
167 for (const CatalogEntry &e : entries) {
168 ContentBlock item;
169 item.type = BlockType::ListItem;
170 if (!e.href.isEmpty())
171 item.inlineContent.append(makeLink(e.name, e.href));
172 else
173 item.inlineContent.append(makeText(e.name));
174 list.children.append(std::move(item));
175 }
176 return list;
177}
178
179ContentBlock makeCatalog(ListPlaceholderVariant variant)
180{
181 ContentBlock catalog;
182 catalog.type = BlockType::Catalog;
183 catalog.attributes["variant"_L1] = toString(variant);
184 return catalog;
185}
186
187Qt::SortOrder readSortOrder(const ContentBlock &placeholder)
188{
189 return placeholder.attributes.value("sort"_L1).toString() == u"descending"_s
190 ? Qt::DescendingOrder : Qt::AscendingOrder;
191}
192
193void notifyEmpty(const ListExpanderCallbacks &cb, const QString &argument,
194 ListPlaceholderVariant variant)
195{
196 if (cb.onEmpty)
197 cb.onEmpty(argument, variant);
198}
199
200std::optional<ContentBlock> expandAnnotatedClasses(
201 const ListExpanderCallbacks &cb, const ContentBlock &placeholder,
202 const Node *relative)
203{
204 const QString argument = placeholder.attributes.value("argument"_L1).toString();
205 QList<CatalogEntry> entries = cb.collectCppClasses
206 ? cb.collectCppClasses(relative, readSortOrder(placeholder))
207 : QList<CatalogEntry>{};
208
209 if (entries.isEmpty()) {
210 notifyEmpty(cb, argument, ListPlaceholderVariant::AnnotatedClasses);
211 return std::nullopt;
212 }
213
214 ContentBlock catalog = makeCatalog(ListPlaceholderVariant::AnnotatedClasses);
215 catalog.children.append(makeAnnotatedTable(entries));
216 return catalog;
217}
218
219std::optional<ContentBlock> expandAnnotatedExamples(
220 const ListExpanderCallbacks &cb, const ContentBlock &placeholder,
221 const Node *relative)
222{
223 const QString argument = placeholder.attributes.value("argument"_L1).toString();
224 QList<CatalogEntryGroup> groups = cb.collectExamplesGrouped
225 ? cb.collectExamplesGrouped(relative)
226 : QList<CatalogEntryGroup>{};
227
228 ContentBlock catalog = makeCatalog(ListPlaceholderVariant::AnnotatedExamples);
229 for (const CatalogEntryGroup &g : groups) {
230 // Skip groups that came in pre-emptied: the production
231 // CatalogEntrySource drops them, but a less-strict callback
232 // could synthesize a heading-with-empty-table shell. Guard
233 // the contract here so the IR never carries one.
234 if (g.entries.isEmpty())
235 continue;
236 // Empty-label groups omit the heading and render the table
237 // alone. This matches HtmlGenerator::generateAnnotatedLists,
238 // which has always guarded the <h2> emission against empty
239 // multimap keys: examples bucketed under an empty Tree
240 // indexTitle still appear in the rendered table, but the
241 // headingless slot above them carries no decorative noise.
242 if (!g.label.isEmpty())
243 catalog.children.append(makeSectionHeading(2, g.label, g.anchorId));
244 catalog.children.append(makeAnnotatedTable(g.entries));
245 }
246
247 if (catalog.children.isEmpty()) {
248 notifyEmpty(cb, argument, ListPlaceholderVariant::AnnotatedExamples);
249 return std::nullopt;
250 }
251 return catalog;
252}
253
254std::optional<ContentBlock> expandCompactClasses(
255 const ListExpanderCallbacks &cb, const ContentBlock &placeholder,
256 const Node *relative)
257{
258 const QString argument = placeholder.attributes.value("argument"_L1).toString();
259 const QString rootName = placeholder.attributes.value("rootName"_L1).toString();
260
261 QList<CatalogEntry> entries = cb.collectCompactClasses
262 ? cb.collectCompactClasses(relative, rootName)
263 : QList<CatalogEntry>{};
264
265 if (entries.isEmpty()) {
266 notifyEmpty(cb, argument, ListPlaceholderVariant::CompactClasses);
267 return std::nullopt;
268 }
269
270 ContentBlock catalog = makeCatalog(ListPlaceholderVariant::CompactClasses);
271 if (!rootName.isEmpty())
272 catalog.attributes["rootName"_L1] = rootName;
273 catalog.children.append(makeCompactList(entries));
274 return catalog;
275}
276
277std::optional<ContentBlock> expandAnnotatedGroup(
278 const ListExpanderCallbacks &cb, const ContentBlock &placeholder,
279 const Node *relative)
280{
281 const QString argument = placeholder.attributes.value("argument"_L1).toString();
282 QList<CatalogEntry> entries = cb.collectGroupMembers
283 ? cb.collectGroupMembers(relative, argument, readSortOrder(placeholder))
284 : QList<CatalogEntry>{};
285
286 if (entries.isEmpty()) {
287 notifyEmpty(cb, argument, ListPlaceholderVariant::AnnotatedGroup);
288 return std::nullopt;
289 }
290
291 ContentBlock catalog = makeCatalog(ListPlaceholderVariant::AnnotatedGroup);
292 catalog.attributes["argument"_L1] = argument;
293 catalog.children.append(makeAnnotatedTable(entries));
294 return catalog;
295}
296
297std::optional<ContentBlock> expandPlaceholder(
298 const ListExpanderCallbacks &cb, const ContentBlock &placeholder,
299 const Node *relative)
300{
301 const auto variant = parseListPlaceholderVariant(
302 placeholder.attributes.value("variant"_L1).toString());
303 if (!variant)
304 return std::nullopt;
305
306 switch (*variant) {
307 case ListPlaceholderVariant::AnnotatedClasses:
308 return expandAnnotatedClasses(cb, placeholder, relative);
309 case ListPlaceholderVariant::AnnotatedExamples:
310 return expandAnnotatedExamples(cb, placeholder, relative);
311 case ListPlaceholderVariant::CompactClasses:
312 return expandCompactClasses(cb, placeholder, relative);
313 case ListPlaceholderVariant::AnnotatedGroup:
314 return expandAnnotatedGroup(cb, placeholder, relative);
315 }
316 Q_UNREACHABLE_RETURN(std::nullopt);
317}
318
319} // anonymous namespace
320
321ListExpander::ListExpander(ListExpanderCallbacks callbacks)
322 : m_callbacks(std::move(callbacks))
323{
324}
325
326/*!
327 Walks \a blocks in place, replacing every BlockType::ListPlaceholder
328 with a populated BlockType::Catalog subtree, or removing the
329 placeholder when the enumeration is empty. Empty enumerations
330 invoke \c{ListExpanderCallbacks::onEmpty} (when set) so the driver
331 can warn the author. Recurses into \c{block.children} so placeholders
332 nested inside sections or other container blocks are expanded too.
333
334 The \a relative pointer is forwarded as opaque state to the
335 callbacks, which use it to resolve relative-aware display names
336 and hrefs.
337*/
338void ListExpander::expand(QList<ContentBlock> &blocks, const Node *relative)
339{
340 for (qsizetype i = 0; i < blocks.size(); ) {
341 if (blocks[i].type == BlockType::ListPlaceholder) {
342 auto expanded = expandPlaceholder(m_callbacks, blocks[i], relative);
343 if (expanded) {
344 blocks.replace(i, std::move(*expanded));
345 ++i;
346 } else {
347 blocks.removeAt(i);
348 }
349 } else {
350 expand(blocks[i].children, relative);
351 ++i;
352 }
353 }
354}
355
356} // namespace IR
357
358QT_END_NAMESPACE
359
360#endif // QDOC_TEMPLATE_GENERATOR_ENABLED