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
contentbuilder.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
5
6#include "../atom.h"
7
9
10using namespace Qt::Literals::StringLiterals;
11
12namespace IR {
13
14/*!
15 \class IR::ContentBuilder
16 \internal
17 \brief Converts Atom chains to QList<IR::ContentBlock> trees.
18
19 ContentBuilder walks a linked list of Atom nodes (QDoc's internal
20 documentation representation) and produces a structured tree of
21 ContentBlock and InlineContent values suitable for template rendering.
22
23 Handled atom types:
24 \list
25 \li ParaLeft, ParaRight -- Paragraph blocks.
26 \li String -- Text inline content.
27 \li C -- Inline code spans.
28 \li Code, CodeBad, Qml -- Code blocks with language attribute.
29 \li SectionLeft, SectionRight -- Section containers.
30 \li SectionHeadingLeft, SectionHeadingRight -- Section headings with
31 level.
32 \li FormattingLeft, FormattingRight -- Bold, italic, teletype,
33 underline, subscript, superscript, parameter, uicontrol, trademark,
34 link, index, notranslate, span.
35 \li ListLeft, ListRight -- Ordered and unordered lists.
36 \li ListItemLeft, ListItemRight -- List items.
37 \li ListItemNumber -- List start number metadata.
38 \li NoteLeft, NoteRight -- Note admonition blocks.
39 \li WarningLeft, WarningRight -- Warning admonition blocks.
40 \li BriefLeft, BriefRight -- Brief exclusion (skipped in body).
41 \li Link, NavLink -- Explicit links with unresolved target.
42 \li AutoLink, NavAutoLink -- Auto-linked type names with unresolved
43 target.
44 \li BR -- Line break inline.
45 \li HR -- Horizontal rule block.
46 \li Nop -- No-operation (skipped).
47 \li BaseName -- No-operation (skipped).
48 \li TableLeft, TableRight -- Table containers with style attribute.
49 \li TableHeaderLeft, TableHeaderRight -- Table header rows.
50 \li TableRowLeft, TableRowRight -- Table data rows.
51 \li TableItemLeft, TableItemRight -- Table cells with optional
52 colspan/rowspan.
53 \li ListTagLeft, ListTagRight -- Value list tag items (ListItem
54 blocks).
55 \li SinceTagLeft, SinceTagRight -- Version tag items (skipped).
56 \li AnnotatedList -- Placeholder Div.
57 \li GeneratedList -- Placeholder Div.
58 \endlist
59
60 Format-conditional atoms (FormatIf, FormatElse, FormatEndif) are
61 skipped unconditionally. The template generator builds a
62 format-agnostic IR that serves all output formats from a single
63 build pass.
64
65 ContentBuilder depends only on Atom (for reading the chain) and IR types
66 (for producing output).
67
68 \sa ContentBlock, InlineContent
69*/
70
71static InlineType formattingToInlineType(const QString &formatting)
72{
73 if (formatting == ATOM_FORMATTING_BOLD)
74 return InlineType::Bold;
75 if (formatting == ATOM_FORMATTING_ITALIC)
76 return InlineType::Italic;
77 if (formatting == ATOM_FORMATTING_TELETYPE)
79 if (formatting == ATOM_FORMATTING_UNDERLINE)
81 if (formatting == ATOM_FORMATTING_SUBSCRIPT)
83 if (formatting == ATOM_FORMATTING_SUPERSCRIPT)
85 if (formatting == ATOM_FORMATTING_PARAMETER)
87 return InlineType::Text;
88}
89
90/*!
91 Walks the atom chain starting at \a firstAtom and returns a list
92 of ContentBlock trees representing the structured documentation body.
93
94 Returns an empty list if \a firstAtom is \nullptr.
95
96 The builder is reset before processing, so a single ContentBuilder
97 instance can be reused for multiple build() calls.
98*/
100{
101 m_result.clear();
102 m_blockPath.clear();
103 m_inlinePath.clear();
104 m_inlineBaseDepths.clear();
105 m_inBrief = false;
106 m_inLink = false;
107
108 if (!firstAtom)
109 return {};
110
111 processAtoms(firstAtom);
112
113 // Malformed atom chain recovery: auto-close remaining blocks in
114 // release builds, assert in debug to surface the source error.
115 if (Q_UNLIKELY(!m_blockPath.isEmpty())) {
116 Q_ASSERT_X(false, "ContentBuilder::build",
117 "Unclosed blocks at end of atom chain");
118 while (!m_blockPath.isEmpty())
119 closeBlock();
120 }
121
122 Q_ASSERT(m_inlinePath.isEmpty());
123 Q_ASSERT(m_inlineBaseDepths.isEmpty());
124 Q_ASSERT(!m_inLink);
125 Q_ASSERT(!m_inBrief);
126
127 return m_result;
128}
129
130/*!
131 Walks the full atom chain starting at \a atom, building the
132 content tree. FormatIf..FormatEndif blocks are skipped
133 unconditionally via skipFormatIfBlock(). Stray FormatElse and
134 FormatEndif atoms outside any FormatIf context are ignored.
135*/
136void ContentBuilder::processAtoms(const Atom *atom)
137{
138 while (atom) {
139 if (atom->type() == Atom::FormatIf) {
140 atom = skipFormatIfBlock(atom);
141 continue;
142 }
144 atom = atom->next();
145 continue;
146 }
147 atom = dispatchAtom(atom);
148 if (!atom)
149 return;
150 atom = atom->next();
151 }
152}
153
154/*!
155 Skips an entire FormatIf..FormatEndif block, including any nested
156 FormatIf blocks and FormatElse branches. The scan only tracks
157 FormatIf and FormatEndif for depth counting; all other atom types
158 (including FormatElse) are treated as inert content and walked
159 past without dispatch.
160
161 The template generator builds a format-agnostic IR, so
162 format-conditional content is unconditionally excluded.
163
164 Returns a pointer to the atom after FormatEndif, or \nullptr if
165 the chain ends before FormatEndif is found.
166*/
167const Atom *ContentBuilder::skipFormatIfBlock(const Atom *atom)
168{
169 Q_ASSERT(atom->type() == Atom::FormatIf);
170 int depth = 1;
171 atom = atom->next();
172 while (atom && depth > 0) {
173 if (atom->type() == Atom::FormatIf)
174 ++depth;
175 else if (atom->type() == Atom::FormatEndif)
176 --depth;
177 atom = atom->next();
178 }
179 return atom;
180}
181
182/*!
183 Dispatches a single atom to the content model. Returns the last
184 atom consumed — usually \a atom itself, but some atom types may
185 consume subsequent atoms.
186*/
187const Atom *ContentBuilder::dispatchAtom(const Atom *atom)
188{
189 if (atom->type() == Atom::BriefLeft) {
190 m_inBrief = true;
191 return atom;
192 }
193 if (atom->type() == Atom::BriefRight) {
194 m_inBrief = false;
195 return atom;
196 }
197 if (m_inBrief)
198 return atom;
199
200 switch (atom->type()) {
201
202 case Atom::ParaLeft:
203 openBlock(BlockType::Paragraph);
204 break;
205
206 case Atom::ParaRight:
207 closeBlock();
208 break;
209
210 case Atom::String:
211 addLeafInline(InlineType::Text, atom->string());
212 break;
213
214 case Atom::C:
215 addLeafInline(InlineType::Code, atom->string());
216 break;
217
218 case Atom::Code:
219 case Atom::CodeBad:
220 case Atom::Qml: {
221 QJsonObject attrs;
222 if (atom->type() == Atom::Qml) {
223 attrs["language"_L1] = u"qml"_s;
224 } else if (atom->type() == Atom::CodeBad) {
225 attrs["language"_L1] = u"cpp"_s;
226 attrs["bad"_L1] = true;
227 } else if (atom->count() >= 2 && !atom->string(1).isEmpty()) {
228 attrs["language"_L1] = atom->string(1);
229 } else {
230 attrs["language"_L1] = u"cpp"_s;
231 }
232
233 openBlock(BlockType::CodeBlock, attrs);
234 addLeafInline(InlineType::Text, atom->string());
235 closeBlock();
236 break;
237 }
238
239 case Atom::AutoLink:
240 case Atom::NavAutoLink: {
241 // href values are not author-controlled. They are produced by QDoc
242 // link resolution (\l, autolinks) against the node tree, or by
243 // \image path handling. They don't contain arbitrary schemes (e.g.
244 // javascript:). Only link text originates from user-authored docs
245 // and must be HTML-escaped in templates.
246 InlineContent link;
248 link.href = atom->string();
249 link.children.append({ InlineType::Text, atom->string(), {}, {}, {}, {} });
250 addInline(std::move(link));
251 break;
252 }
253
254 case Atom::Link:
255 case Atom::NavLink: {
256 if (Q_UNLIKELY(m_blockPath.isEmpty()))
257 break;
258
259 m_inLink = true;
260
261 InlineContent link;
263 link.href = atom->string();
264
265 pushInlineContainer(std::move(link));
266
267 // Link atoms are always followed by FormattingLeft("link");
268 // skip it to avoid double-processing.
270 && atom->next()->string() == ATOM_FORMATTING_LINK) {
271 return atom->next();
272 }
273 break;
274 }
275
277 const QString &fmt = atom->string();
278
279 if (fmt == ATOM_FORMATTING_INDEX || fmt.startsWith(u"span "_s))
280 break;
281
282 if (fmt == ATOM_FORMATTING_LINK)
283 break;
284
286 break;
287
288 if (fmt == ATOM_FORMATTING_UICONTROL) {
289 InlineContent bold;
291 pushInlineContainer(std::move(bold));
292 break;
293 }
294
295 InlineType type = formattingToInlineType(fmt);
296 if (type == InlineType::Text)
297 break;
298
299 InlineContent container;
300 container.type = type;
301 pushInlineContainer(std::move(container));
302 break;
303 }
304
306 const QString &fmt = atom->string();
307
308 if (fmt == ATOM_FORMATTING_LINK) {
309 if (m_inLink) {
310 const qsizetype base = m_inlineBaseDepths.isEmpty() ? 0 : m_inlineBaseDepths.last();
311 if (m_inlinePath.size() > base) {
312 Q_ASSERT(resolveInline()->type == InlineType::Link);
313 m_inlinePath.removeLast();
314 }
315 m_inLink = false;
316 }
317 break;
318 }
319
321 || fmt.startsWith(u"span "_s) || fmt == ATOM_FORMATTING_TRADEMARK) {
322 break;
323 }
324
325 const qsizetype base = m_inlineBaseDepths.isEmpty() ? 0 : m_inlineBaseDepths.last();
326 if (m_inlinePath.size() > base)
327 m_inlinePath.removeLast();
328 break;
329 }
330
331 case Atom::SectionLeft:
332 openBlock(BlockType::Section);
333 break;
334
336 closeBlock();
337 break;
338
340 QJsonObject attrs;
341 attrs["level"_L1] = atom->string().toInt();
342 openBlock(BlockType::SectionHeading, attrs);
343 break;
344 }
345
347 closeBlock();
348 break;
349
350 case Atom::ListLeft: {
351 QJsonObject attrs;
352 attrs["listType"_L1] = atom->string();
353 openBlock(BlockType::List, attrs);
354 break;
355 }
356
357 case Atom::ListRight:
358 closeBlock();
359 break;
360
361 case Atom::ListItemLeft:
362 openBlock(BlockType::ListItem);
363 break;
364
366 closeBlock();
367 break;
368
370 // Start-number metadata is not yet represented in the IR.
371 break;
372
373 case Atom::NoteLeft:
374 openBlock(BlockType::Note);
375 break;
376
377 case Atom::NoteRight:
378 closeBlock();
379 break;
380
381 case Atom::WarningLeft:
382 openBlock(BlockType::Warning);
383 break;
384
386 closeBlock();
387 break;
388
389 case Atom::BR:
390 addLeafInline(InlineType::LineBreak, {});
391 break;
392
393 case Atom::HR:
394 openBlock(BlockType::HorizontalRule);
395 closeBlock();
396 break;
397
398 case Atom::AnnotatedList: {
399 QJsonObject attrs;
400 attrs["annotatedList"_L1] = atom->string();
401 openBlock(BlockType::Div, attrs);
402 closeBlock();
403 break;
404 }
405
406 case Atom::GeneratedList: {
407 QJsonObject attrs;
408 attrs["generatedList"_L1] = atom->string();
409 openBlock(BlockType::Div, attrs);
410 closeBlock();
411 break;
412 }
413
414 case Atom::TableLeft: {
415 QJsonObject attrs;
416 const QString &style = atom->string();
417 attrs["style"_L1] = style == "borderless"_L1 ? style : u"generic"_s;
418 openBlock(BlockType::Table, attrs);
419 break;
420 }
421
422 case Atom::TableRight:
423 closeBlock();
424 break;
425
426 case Atom::TableHeaderLeft:
427 openBlock(BlockType::TableHeaderRow);
428 break;
429
431 closeBlock();
432 break;
433
434 case Atom::TableRowLeft:
435 openBlock(BlockType::TableRow);
436 break;
437
439 closeBlock();
440 break;
441
442 case Atom::TableItemLeft: {
443 QJsonObject attrs;
444 const QString &spec = atom->string();
445 if (!spec.isEmpty()) {
446 const auto parts = QStringView{spec}.split(u',');
447 if (parts.size() >= 2) {
448 int colspan = qMax(1, parts[0].toInt());
449 int rowspan = qMax(1, parts[1].toInt());
450 if (colspan > 1)
451 attrs["colspan"_L1] = colspan;
452 if (rowspan > 1)
453 attrs["rowspan"_L1] = rowspan;
454 }
455 }
456 openBlock(BlockType::TableCell, attrs);
457 break;
458 }
459
461 closeBlock();
462 break;
463
464 case Atom::ListTagLeft:
465 openBlock(BlockType::ListItem);
466 break;
467
469 closeBlock();
470 break;
471
474 break;
475
476 case Atom::Nop:
477 case Atom::BaseName:
478 break;
479
480 default:
481 break;
482 }
483 return atom;
484}
485
486/*!
487 Opens a new block of type \a type with optional \a attrs.
488
489 If the block path is empty, the block is added to the top-level
490 result list. Otherwise it is added as a child of the current
491 container block.
492*/
493void ContentBuilder::openBlock(BlockType type, QJsonObject attrs)
494{
495 ContentBlock block;
496 block.type = type;
497 block.attributes = std::move(attrs);
498
499 m_inlineBaseDepths.append(m_inlinePath.size());
500
501 if (m_blockPath.isEmpty()) {
502 m_result.append(std::move(block));
503 m_blockPath.append(m_result.size() - 1);
504 } else {
505 auto *parent = resolveBlock();
506 parent->children.append(std::move(block));
507 m_blockPath.append(parent->children.size() - 1);
508 }
509}
510
511/*!
512 Closes the current block by popping it from the block path.
513
514 Verifies that the inline path depth matches the depth recorded
515 when this block was opened (all formatting pairs balanced).
516 In release builds, the inline path is restored to the expected
517 depth as a safety measure against malformed atom chains.
518*/
519void ContentBuilder::closeBlock()
520{
521 if (!m_blockPath.isEmpty()) {
522 if (Q_UNLIKELY(m_inlineBaseDepths.isEmpty())) {
523 // openBlock() always pushes a base depth, so this indicates
524 // a logic error in the builder. Assert in debug; recover in
525 // release by clearing all state.
526 Q_ASSERT_X(false, "ContentBuilder::closeBlock",
527 "m_inlineBaseDepths empty with non-empty m_blockPath");
528 m_inlinePath.clear();
529 m_blockPath.clear();
530 m_inlineBaseDepths.clear();
531 m_inLink = false;
532 return;
533 }
534 const qsizetype expectedDepth = m_inlineBaseDepths.last();
535 if (m_inLink && m_inlinePath.size() > expectedDepth)
536 m_inLink = false;
537 Q_ASSERT(m_inlinePath.size() == expectedDepth);
538 m_inlinePath.resize(expectedDepth);
539 m_inlineBaseDepths.removeLast();
540 m_blockPath.removeLast();
541 }
542}
543
544/*!
545 Adds \a inline_ to the current block's inline content.
546
547 If there is an active inline container (from FormattingLeft or
548 Link atom), the inline is added to that container's children
549 instead.
550
551 If no block is open, the inline is dropped. This shouldn't happen
552 with well-formed atom chains (text is always wrapped in
553 ParaLeft/ParaRight), and is asserted in debug builds.
554*/
555void ContentBuilder::addInline(InlineContent inline_)
556{
557 if (!m_inlinePath.isEmpty()) {
558 resolveInline()->children.append(std::move(inline_));
559 } else if (!m_blockPath.isEmpty()) {
560 resolveBlock()->inlineContent.append(std::move(inline_));
561 } else {
562 // Inline content without an enclosing block is dropped.
563 // QDoc's atom chains always wrap text in ParaLeft/ParaRight,
564 // so this path indicates a malformed chain.
565 Q_ASSERT_X(false, "ContentBuilder::addInline",
566 "Inline content without an enclosing block");
567 }
568}
569
570/*!
571 Convenience method: creates a leaf InlineContent of the given
572 \a type with \a text and appends it via addInline().
573*/
574void ContentBuilder::addLeafInline(InlineType type, const QString &text)
575{
576 InlineContent ic;
577 ic.type = type;
578 ic.text = text;
579 addInline(std::move(ic));
580}
581
582/*!
583 Pushes \a container as a new inline container. Subsequent
584 addInline() and pushInlineContainer() calls will nest
585 their content inside this container.
586
587 Unlike addInline(), which only appends leaf inlines, this method
588 also updates m_inlinePath to enable nesting. It respects the
589 existing inline path: if we are already inside a Link or formatting
590 container, the new container is added as a child of that container.
591*/
592void ContentBuilder::pushInlineContainer(InlineContent container)
593{
594 if (!m_inlinePath.isEmpty()) {
595 auto *parent = resolveInline();
596 parent->children.append(std::move(container));
597 m_inlinePath.append(parent->children.size() - 1);
598 } else if (!m_blockPath.isEmpty()) {
599 auto *block = resolveBlock();
600 block->inlineContent.append(std::move(container));
601 m_inlinePath.append(block->inlineContent.size() - 1);
602 }
603}
604
605/*!
606 Resolves the current block path to a ContentBlock pointer.
607
608 The returned pointer is valid only until the next QList mutation
609 on any list in the path. Callers must use the pointer within a
610 single expression and discard it before appending to any QList.
611*/
612ContentBlock *ContentBuilder::resolveBlock()
613{
614 Q_ASSERT(!m_blockPath.isEmpty());
615 Q_ASSERT(m_blockPath[0] >= 0 && m_blockPath[0] < m_result.size());
616 ContentBlock *block = &m_result[m_blockPath[0]];
617 for (qsizetype i = 1; i < m_blockPath.size(); ++i) {
618 Q_ASSERT(m_blockPath[i] >= 0 && m_blockPath[i] < block->children.size());
619 block = &block->children[m_blockPath[i]];
620 }
621 return block;
622}
623
624/*!
625 Resolves the current inline path to an InlineContent pointer.
626
627 Walks the block path first (via resolveBlock()), then descends
628 through the block's inlineContent and nested children lists
629 using the indices in m_inlinePath.
630*/
631InlineContent *ContentBuilder::resolveInline()
632{
633 ContentBlock *block = resolveBlock();
634 Q_ASSERT(!m_inlinePath.isEmpty());
635 Q_ASSERT(m_inlinePath[0] >= 0 && m_inlinePath[0] < block->inlineContent.size());
636 InlineContent *ic = &block->inlineContent[m_inlinePath[0]];
637 for (qsizetype i = 1; i < m_inlinePath.size(); ++i) {
638 Q_ASSERT(m_inlinePath[i] >= 0 && m_inlinePath[i] < ic->children.size());
639 ic = &ic->children[m_inlinePath[i]];
640 }
641 return ic;
642}
643
644} // namespace IR
645
646QT_END_NAMESPACE
#define ATOM_FORMATTING_TELETYPE
Definition atom.h:211
#define ATOM_FORMATTING_UNDERLINE
Definition atom.h:214
#define ATOM_FORMATTING_NOTRANSLATE
Definition atom.h:206
#define ATOM_FORMATTING_SUBSCRIPT
Definition atom.h:209
#define ATOM_FORMATTING_BOLD
Definition atom.h:202
#define ATOM_FORMATTING_TRADEMARK
Definition atom.h:212
#define ATOM_FORMATTING_ITALIC
Definition atom.h:204
#define ATOM_FORMATTING_LINK
Definition atom.h:205
#define ATOM_FORMATTING_SUPERSCRIPT
Definition atom.h:210
#define ATOM_FORMATTING_INDEX
Definition atom.h:203
#define ATOM_FORMATTING_UICONTROL
Definition atom.h:213
#define ATOM_FORMATTING_PARAMETER
Definition atom.h:207
The Atom class is the fundamental unit for representing documents internally.
Definition atom.h:19
AtomType type() const
Return the type of this atom.
Definition atom.h:153
@ TableRight
Definition atom.h:95
@ GeneratedList
Definition atom.h:50
@ BriefRight
Definition atom.h:27
@ TableHeaderRight
Definition atom.h:97
@ FormatElse
Definition atom.h:45
@ TableRowRight
Definition atom.h:99
@ Nop
Definition atom.h:72
@ WarningRight
Definition atom.h:109
@ ListTagRight
Definition atom.h:66
@ NavLink
Definition atom.h:71
@ ListItemNumber
Definition atom.h:64
@ SinceTagRight
Definition atom.h:89
@ CodeBad
Definition atom.h:32
@ AnnotatedList
Definition atom.h:22
@ SectionRight
Definition atom.h:82
@ SectionHeadingLeft
Definition atom.h:83
@ TableLeft
Definition atom.h:94
@ ListItemRight
Definition atom.h:68
@ TableItemRight
Definition atom.h:101
@ Code
Definition atom.h:31
@ ListLeft
Definition atom.h:63
@ NavAutoLink
Definition atom.h:70
@ BriefLeft
Definition atom.h:26
@ ListRight
Definition atom.h:69
@ ParaRight
Definition atom.h:76
@ Qml
Definition atom.h:77
@ FormattingLeft
Definition atom.h:48
@ FormattingRight
Definition atom.h:49
@ SectionHeadingRight
Definition atom.h:84
@ Link
Definition atom.h:61
@ FormatEndif
Definition atom.h:46
@ SinceTagLeft
Definition atom.h:88
@ AutoLink
Definition atom.h:23
@ TableItemLeft
Definition atom.h:100
@ NoteRight
Definition atom.h:74
@ BaseName
Definition atom.h:24
@ FormatIf
Definition atom.h:47
const Atom * next() const
Return the next atom in the atom list.
Definition atom.h:150
Converts Atom chains to QList<IR::ContentBlock> trees.
QList< ContentBlock > build(const Atom *firstAtom)
Walks the atom chain starting at firstAtom and returns a list of ContentBlock trees representing the ...
Definition builder.cpp:14
BlockType
static InlineType formattingToInlineType(const QString &formatting)
InlineType
Combined button and popup list for selecting options.
Represents a structural block element in documentation.
Represents inline content within a documentation block.