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
7#include "sorting.h"
8
9#include "../atom.h"
10#include "../tree.h"
11
13
14using namespace Qt::Literals::StringLiterals;
15
16static QString stripCodeMarkerTags(const QString &markedCode)
17{
18 static const QRegularExpression tag(u"</?@[^>]*>"_s);
19 QString t = markedCode;
20 t.replace(tag, QString());
21 t.replace(u"&quot;"_s, u"\""_s);
22 t.replace(u"&gt;"_s, u">"_s);
23 t.replace(u"&lt;"_s, u"<"_s);
24 t.replace(u"&amp;"_s, u"&"_s);
25 return t;
26}
27
28static QString genusToString(Genus genus)
29{
30 switch (genus) {
31 case Genus::CPP: return u"cpp"_s;
32 case Genus::QML: return u"qml"_s;
33 case Genus::DOC: return u"doc"_s;
34 case Genus::API: return u"api"_s;
35 case Genus::DontCare: return {};
36 }
37 return {};
38}
39
41{
42 // DocParser is the only producer of Atom::AnnotatedList and
43 // Atom::GeneratedList, and both build them with at most two
44 // strings: the command argument at index 0 and the optional
45 // bracketed sort directive at index 1. Reading args.at(1) is
46 // therefore the sort slot, not "the last string."
47 const QStringList &args = atom->strings();
48 const QString directive = args.size() > 1 ? args.at(1) : QString();
49 return Sorting::parseSortOrder(directive) == Qt::DescendingOrder
50 ? u"descending"_s : u"ascending"_s;
51}
52
53namespace IR {
54
55/*!
56 \class IR::ContentBuilder
57 \internal
58 \brief Converts Atom chains to QList<IR::ContentBlock> trees.
59
60 ContentBuilder walks a linked list of Atom nodes (QDoc's internal
61 documentation representation) and produces a structured tree of
62 ContentBlock and InlineContent values suitable for template rendering.
63
64 Handled atom types:
65 \list
66 \li ParaLeft, ParaRight -- Paragraph blocks.
67 \li String -- Text inline content.
68 \li C -- Inline code spans.
69 \li Code, CodeBad, Qml -- Code blocks with language attribute.
70 \li SectionLeft, SectionRight -- Section containers.
71 \li SectionHeadingLeft, SectionHeadingRight -- Section headings with
72 level.
73 \li FormattingLeft, FormattingRight -- Bold, italic, teletype,
74 underline, subscript, superscript, parameter, uicontrol, trademark,
75 link, index, notranslate, span.
76 \li ListLeft, ListRight -- Ordered and unordered lists.
77 \li ListItemLeft, ListItemRight -- List items.
78 \li ListItemNumber -- List start number metadata.
79 \li NoteLeft, NoteRight -- Note admonition blocks.
80 \li WarningLeft, WarningRight -- Warning admonition blocks.
81 \li BriefLeft, BriefRight -- Brief exclusion (skipped by default,
82 or emitted as Paragraph with BriefHandling::Include).
83 \li Link, NavLink -- Explicit links with unresolved target.
84 \li AutoLink, NavAutoLink -- Auto-linked type names with unresolved
85 target.
86 \li BR -- Line break inline.
87 \li HR -- Horizontal rule block.
88 \li Nop -- No-operation (skipped).
89 \li BaseName -- No-operation (skipped).
90 \li TableLeft, TableRight -- Table containers with style attribute.
91 \li TableHeaderLeft, TableHeaderRight -- Table header rows.
92 \li TableRowLeft, TableRowRight -- Table data rows.
93 \li TableItemLeft, TableItemRight -- Table cells with optional
94 colspan/rowspan.
95 \li Image -- Block-level image (wrapping Paragraph with centerAlign).
96 \li InlineImage -- Inline image within a paragraph.
97 \li ImageText -- Alt text consumed by the preceding Image or
98 InlineImage handler.
99 \li ListTagLeft, ListTagRight -- Value list tag items (ListItem
100 blocks).
101 \li SinceTagLeft, SinceTagRight -- Version tag items (skipped).
102 \li AnnotatedList -- Typed ListPlaceholder block carrying a
103 \c{ListPlaceholderVariant} discriminator, the group
104 argument, and a parsed sort directive.
105 \li GeneratedList -- Typed ListPlaceholder block for the
106 recognized variants (annotatedexamples, annotatedclasses,
107 classes, classes \e {<rootname>}); a Div fallback retains
108 the legacy attribute bag for unknown variants.
109 \endlist
110
111 Format-conditional atoms (FormatIf, FormatElse, FormatEndif) are
112 skipped unconditionally. The template generator builds a
113 format-agnostic IR that serves all output formats from a single
114 build pass.
115
116 The optional BriefHandling parameter controls whether brief
117 content (between BriefLeft and BriefRight atoms) is included in
118 the output. The default is BriefHandling::Skip, which suppresses
119 brief content. BriefHandling::Include causes brief content to be
120 emitted as a Paragraph block.
121
122 ContentBuilder depends only on Atom (for reading the chain) and IR types
123 (for producing output).
124
125 \sa ContentBlock, InlineContent
126*/
127
128/*!
129 Constructs a ContentBuilder.
130
131 The \a briefHandling parameter controls whether content between
132 BriefLeft and BriefRight atoms is emitted as a Paragraph block
133 (BriefHandling::Include) or suppressed (BriefHandling::Skip).
134
135 The \a headingOffset parameter shifts section heading levels to
136 account for the page structure. QDoc's \\section1 maps to level 1,
137 but pages already use \c{<h1>} for the title and \c{<h2>} for
138 major sections. The legacy generators apply an offset derived from
139 the node type; callers pass that same offset here so the IR
140 produces correct heading levels without depending on \b Node.
141*/
142ContentBuilder::ContentBuilder(BriefHandling briefHandling, int headingOffset,
143 DiagnosticHandler diagnosticHandler)
144 : m_briefHandling(briefHandling), m_headingOffset(headingOffset),
146{
147}
148
149static InlineType formattingToInlineType(const QString &formatting)
150{
151 if (formatting == ATOM_FORMATTING_BOLD)
152 return InlineType::Bold;
153 if (formatting == ATOM_FORMATTING_ITALIC)
154 return InlineType::Italic;
155 if (formatting == ATOM_FORMATTING_TELETYPE)
157 if (formatting == ATOM_FORMATTING_UNDERLINE)
159 if (formatting == ATOM_FORMATTING_SUBSCRIPT)
161 if (formatting == ATOM_FORMATTING_SUPERSCRIPT)
163 if (formatting == ATOM_FORMATTING_PARAMETER)
165 return InlineType::Text;
166}
167
168/*!
169 Walks the atom chain starting at \a firstAtom and returns a list
170 of ContentBlock trees representing the structured documentation body.
171
172 Returns an empty list if \a firstAtom is \nullptr.
173
174 The builder is reset before processing, so a single ContentBuilder
175 instance can be reused for multiple build() calls.
176*/
178{
179 m_result.clear();
180 m_blockPath.clear();
181 m_inlinePath.clear();
182 m_inlineBaseDepths.clear();
183 m_inBrief = false;
184 m_inLink = false;
185
186 if (!firstAtom)
187 return {};
188
189 processAtoms(firstAtom);
190
191 // Malformed atom chain recovery: auto-close remaining blocks in
192 // release builds, assert in debug to surface the source error.
193 while (!m_blockPath.isEmpty())
194 closeBlock();
195 m_inlinePath.clear();
196 m_inlineBaseDepths.clear();
197 m_inLink = false;
198 m_inBrief = false;
199
200 return m_result;
201}
202
203/*!
204 Walks the full atom chain starting at \a atom, building the
205 content tree. FormatIf..FormatEndif blocks are skipped
206 unconditionally via skipFormatIfBlock(). Stray FormatElse and
207 FormatEndif atoms outside any FormatIf context are ignored.
208*/
209void ContentBuilder::processAtoms(const Atom *atom)
210{
211 while (atom) {
212 if (atom->type() == Atom::FormatIf) {
213 atom = skipFormatIfBlock(atom);
214 continue;
215 }
217 atom = atom->next();
218 continue;
219 }
220 atom = dispatchAtom(atom);
221 if (!atom)
222 return;
223 atom = atom->next();
224 }
225}
226
227/*!
228 Skips an entire FormatIf..FormatEndif block, including any nested
229 FormatIf blocks and FormatElse branches. The scan only tracks
230 FormatIf and FormatEndif for depth counting; all other atom types
231 (including FormatElse) are treated as inert content and walked
232 past without dispatch.
233
234 The template generator builds a format-agnostic IR, so
235 format-conditional content is unconditionally excluded.
236
237 Returns a pointer to the atom after FormatEndif, or \nullptr if
238 the chain ends before FormatEndif is found.
239*/
240const Atom *ContentBuilder::skipFormatIfBlock(const Atom *atom)
241{
242 Q_ASSERT(atom->type() == Atom::FormatIf);
243 int depth = 1;
244 atom = atom->next();
245 while (atom && depth > 0) {
246 if (atom->type() == Atom::FormatIf)
247 ++depth;
248 else if (atom->type() == Atom::FormatEndif)
249 --depth;
250 atom = atom->next();
251 }
252 return atom;
253}
254
255/*!
256 Dispatches a single atom to the content model. Returns the last
257 atom consumed — usually \a atom itself, but some atom types may
258 consume subsequent atoms.
259*/
260const Atom *ContentBuilder::dispatchAtom(const Atom *atom)
261{
262 if (atom->type() == Atom::BriefLeft) {
263 m_inBrief = true;
264 if (m_briefHandling == BriefHandling::Include)
265 openBlock(BlockType::Paragraph);
266 return atom;
267 }
268 if (atom->type() == Atom::BriefRight) {
269 if (m_briefHandling == BriefHandling::Include)
270 closeBlock();
271 m_inBrief = false;
272 return atom;
273 }
274 if (m_inBrief && m_briefHandling != BriefHandling::Include)
275 return atom;
276
277 switch (atom->type()) {
278
279 case Atom::ParaLeft:
280 openBlock(BlockType::Paragraph);
281 break;
282
283 case Atom::ParaRight:
284 closeBlock();
285 break;
286
287 case Atom::String:
288 addLeafInline(InlineType::Text, atom->string());
289 break;
290
291 case Atom::C:
292 addLeafInline(InlineType::Code, stripCodeMarkerTags(atom->string()));
293 break;
294
295 case Atom::Code:
296 case Atom::CodeBad:
297 case Atom::Qml: {
298 QJsonObject attrs;
299 if (atom->type() == Atom::Qml) {
300 attrs["language"_L1] = u"qml"_s;
301 } else if (atom->type() == Atom::CodeBad) {
302 attrs["language"_L1] = u"cpp"_s;
303 attrs["bad"_L1] = true;
304 } else if (atom->count() >= 2 && !atom->string(1).isEmpty()) {
305 attrs["language"_L1] = atom->string(1);
306 } else {
307 attrs["language"_L1] = u"cpp"_s;
308 }
309
310 openBlock(BlockType::CodeBlock, attrs);
311 addLeafInline(InlineType::Text, stripCodeMarkerTags(atom->string()));
312 closeBlock();
313 break;
314 }
315
316 case Atom::AutoLink:
317 case Atom::NavAutoLink: {
318 // href values are not author-controlled. They are produced by QDoc
319 // link resolution (\l, autolinks) against the node tree, or by
320 // \image path handling. They don't contain arbitrary schemes (e.g.
321 // javascript:). Only link text originates from user-authored docs
322 // and must be HTML-escaped in templates.
323 InlineContent link;
325 link.href = atom->string();
327 if (atom->isLinkAtom()) {
328 const auto *linkAtom = static_cast<const LinkAtom *>(atom);
329 link.link->sourceLocation = InlineContent::SourceLocation{
330 linkAtom->location.filePath(),
331 linkAtom->location.lineNo()
332 };
333 }
334 link.children.append({ InlineType::Text, atom->string(), {}, {}, {}, {}, {} });
335 addInline(std::move(link));
336 break;
337 }
338
339 case Atom::Link:
340 case Atom::NavLink: {
341 if (m_blockPath.isEmpty())
342 openBlock(BlockType::Paragraph);
343
344 m_inLink = true;
345
346 InlineContent link;
348 link.href = atom->string();
350
351 // Extract genus and module scope from LinkAtom if available.
352 if (atom->isLinkAtom()) {
353 // genus() and domain() are non-const but non-mutating;
354 // the same const_cast pattern is used in qdocdatabase.cpp.
355 auto *mutableAtom = const_cast<Atom *>(atom);
356 Genus genus = mutableAtom->genus();
357 QString genusStr = genusToString(genus);
358 if (!genusStr.isEmpty())
359 link.attributes["linkGenus"_L1] = genusStr;
360 if (Tree *domain = mutableAtom->domain())
361 link.attributes["linkModule"_L1] = domain->physicalModuleName();
362
363 const auto *linkAtom = static_cast<const LinkAtom *>(mutableAtom);
364 link.link->sourceLocation = InlineContent::SourceLocation{
365 linkAtom->location.filePath(),
366 linkAtom->location.lineNo()
367 };
368 }
369
370 pushInlineContainer(std::move(link));
371
372 // Link atoms are always followed by FormattingLeft("link");
373 // skip it to avoid double-processing.
375 && atom->next()->string() == ATOM_FORMATTING_LINK) {
376 return atom->next();
377 }
378 break;
379 }
380
382 const QString &fmt = atom->string();
383
384 if (fmt == ATOM_FORMATTING_INDEX || fmt.startsWith(u"span "_s))
385 break;
386
387 if (fmt == ATOM_FORMATTING_LINK)
388 break;
389
391 break;
392
393 if (fmt == ATOM_FORMATTING_UICONTROL) {
394 InlineContent bold;
396 pushInlineContainer(std::move(bold));
397 break;
398 }
399
400 InlineType type = formattingToInlineType(fmt);
401 if (type == InlineType::Text)
402 break;
403
404 InlineContent container;
405 container.type = type;
406 pushInlineContainer(std::move(container));
407 break;
408 }
409
411 const QString &fmt = atom->string();
412
413 if (fmt == ATOM_FORMATTING_LINK) {
414 if (m_inLink) {
415 const qsizetype base = m_inlineBaseDepths.isEmpty() ? 0 : m_inlineBaseDepths.last();
416 if (m_inlinePath.size() > base) {
417 Q_ASSERT(resolveInline()->type == InlineType::Link);
418 m_inlinePath.removeLast();
419 }
420 m_inLink = false;
421 }
422 break;
423 }
424
426 || fmt.startsWith(u"span "_s) || fmt == ATOM_FORMATTING_TRADEMARK) {
427 break;
428 }
429
430 const qsizetype base = m_inlineBaseDepths.isEmpty() ? 0 : m_inlineBaseDepths.last();
431 if (m_inlinePath.size() > base)
432 m_inlinePath.removeLast();
433 break;
434 }
435
436 case Atom::SectionLeft:
437 openBlock(BlockType::Section);
438 break;
439
441 closeBlock();
442 break;
443
445 QJsonObject attrs;
446 attrs["level"_L1] = atom->string().toInt() + m_headingOffset;
447 // SectionHeadingLeft carries the heading level at string(0) and the
448 // anchor identifier at string(1). The second payload is populated
449 // only when a heading-level macro (\sectionN) emits an explicit ref.
450 if (atom->count() >= 2)
451 attrs["sectionRef"_L1] = atom->string(1);
452 openBlock(BlockType::SectionHeading, attrs);
453 break;
454 }
455
457 closeBlock();
458 break;
459
460 case Atom::ListLeft: {
461 QJsonObject attrs;
462 const QString &listType = atom->string();
463 attrs["listType"_L1] = listType;
464 if (listType == ATOM_LIST_TAG || listType == ATOM_LIST_VALUE)
465 openBlock(BlockType::DefinitionList, attrs);
466 else
467 openBlock(BlockType::List, attrs);
468 break;
469 }
470
471 case Atom::ListRight:
472 closeBlock();
473 break;
474
475 case Atom::ListItemLeft: {
476 ContentBlock *parent = resolveBlock();
477 if (parent->type == BlockType::DefinitionList)
478 openBlock(BlockType::DefinitionDescription);
479 else
480 openBlock(BlockType::ListItem);
481 break;
482 }
483
485 closeBlock();
486 break;
487
489 // Start-number metadata is not yet represented in the IR.
490 break;
491
492 case Atom::NoteLeft:
493 openBlock(BlockType::Note);
494 break;
495
496 case Atom::NoteRight:
497 closeBlock();
498 break;
499
500 case Atom::WarningLeft:
501 openBlock(BlockType::Warning);
502 break;
503
505 closeBlock();
506 break;
507
508 case Atom::BR:
509 addLeafInline(InlineType::LineBreak, {});
510 break;
511
512 case Atom::HR:
513 openBlock(BlockType::HorizontalRule);
514 closeBlock();
515 break;
516
517 case Atom::AnnotatedList: {
518 QJsonObject attrs;
519 attrs["variant"_L1] = toString(ListPlaceholderVariant::AnnotatedGroup);
520 attrs["argument"_L1] = atom->string();
521 attrs["sort"_L1] = placeholderSortAttribute(atom);
522 openBlock(BlockType::ListPlaceholder, attrs);
523 closeBlock();
524 break;
525 }
526
527 case Atom::GeneratedList: {
528 const QString arg = atom->string();
529 QJsonObject attrs;
530 attrs["argument"_L1] = arg;
531 attrs["sort"_L1] = placeholderSortAttribute(atom);
532
533 static constexpr auto classesPrefix = "classes "_L1;
534 if (arg == "annotatedexamples"_L1) {
535 attrs["variant"_L1] = toString(ListPlaceholderVariant::AnnotatedExamples);
536 } else if (arg == "annotatedclasses"_L1) {
537 attrs["variant"_L1] = toString(ListPlaceholderVariant::AnnotatedClasses);
538 } else if (arg == "classes"_L1 || arg.startsWith(classesPrefix)) {
539 attrs["variant"_L1] = toString(ListPlaceholderVariant::CompactClasses);
540 if (arg.startsWith(classesPrefix)) {
541 const QString rootName = arg.sliced(classesPrefix.size()).trimmed();
542 if (!rootName.isEmpty())
543 attrs["rootName"_L1] = rootName;
544 }
545 } else {
546 // Unknown \generatelist variant: keep the legacy attribute-bag
547 // Div so deferred variants continue to render as they do today.
548 // Remove this branch when those variants acquire their own
549 // ListPlaceholderVariant entries in a follow-up phase.
550 QJsonObject fallback;
551 fallback["generatedList"_L1] = arg;
552 openBlock(BlockType::Div, fallback);
553 closeBlock();
554 break;
555 }
556
557 openBlock(BlockType::ListPlaceholder, attrs);
558 closeBlock();
559 break;
560 }
561
562 case Atom::TableLeft: {
563 QJsonObject attrs;
564 QString tableStyle = u"generic"_s;
565 QString width;
566
567 for (int i = 0; i < atom->count(); ++i) {
568 const QString &arg = atom->string(i);
569 if (arg == "borderless"_L1)
570 tableStyle = arg;
571 else if (arg.contains('%'_L1))
572 width = arg;
573 }
574
575 // Handle "100 %" (space before percent) — the percent arrives
576 // as a separate atom argument, reconstruct the width value.
577 if (width == "%"_L1) {
578 bool ok = false;
579 int pct = atom->string(0).toInt(&ok);
580 width = ok ? QString::number(pct) + '%'_L1 : QString();
581 }
582
583 attrs["style"_L1] = tableStyle;
584 if (!width.isEmpty())
585 attrs["width"_L1] = width;
586 openBlock(BlockType::Table, attrs);
587 break;
588 }
589
590 case Atom::TableRight:
591 closeBlock();
592 break;
593
594 case Atom::TableHeaderLeft:
595 openBlock(BlockType::TableHeaderRow);
596 break;
597
599 closeBlock();
600 break;
601
602 case Atom::TableRowLeft:
603 openBlock(BlockType::TableRow);
604 break;
605
607 closeBlock();
608 break;
609
610 case Atom::TableItemLeft: {
611 QJsonObject attrs;
612 const QString &spec = atom->string();
613 if (!spec.isEmpty()) {
614 const auto parts = QStringView{spec}.split(u',');
615 if (parts.size() >= 2) {
616 int colspan = qMax(1, parts[0].toInt());
617 int rowspan = qMax(1, parts[1].toInt());
618 if (colspan > 1)
619 attrs["colspan"_L1] = colspan;
620 if (rowspan > 1)
621 attrs["rowspan"_L1] = rowspan;
622 }
623 }
624 openBlock(BlockType::TableCell, attrs);
625 break;
626 }
627
629 closeBlock();
630 break;
631
632 case Atom::ListTagLeft:
633 openBlock(BlockType::DefinitionTerm);
634 break;
635
637 closeBlock();
638 break;
639
640 case Atom::Image: {
641 QJsonObject attrs;
642 attrs["class"_L1] = u"centerAlign"_s;
643 openBlock(BlockType::Paragraph, attrs);
644
645 InlineContent img;
647 img.href = atom->string();
648 if (atom->next() && atom->next()->type() == Atom::ImageText)
649 img.title = atom->next()->string();
650 addInline(std::move(img));
651
652 closeBlock();
653
654 if (atom->next() && atom->next()->type() == Atom::ImageText)
655 return atom->next();
656 break;
657 }
658
659 case Atom::InlineImage: {
660 if (Q_UNLIKELY(m_blockPath.isEmpty()))
661 break;
662
663 InlineContent img;
665 img.href = atom->string();
666 if (atom->next() && atom->next()->type() == Atom::ImageText)
667 img.title = atom->next()->string();
668 addInline(std::move(img));
669
670 if (atom->next() && atom->next()->type() == Atom::ImageText)
671 return atom->next();
672 break;
673 }
674
675 case Atom::ImageText:
676 break;
677
680 break;
681
682 case Atom::Nop:
683 case Atom::BaseName:
684 break;
685
686 default:
687 break;
688 }
689 return atom;
690}
691
692/*!
693 Opens a new block of type \a type with optional \a attrs.
694
695 If the block path is empty, the block is added to the top-level
696 result list. Otherwise it is added as a child of the current
697 container block.
698*/
699void ContentBuilder::openBlock(BlockType type, QJsonObject attrs)
700{
701 ContentBlock block;
702 block.type = type;
703 block.attributes = std::move(attrs);
704
705 m_inlineBaseDepths.append(m_inlinePath.size());
706
707 if (m_blockPath.isEmpty()) {
708 m_result.append(std::move(block));
709 m_blockPath.append(m_result.size() - 1);
710 } else {
711 auto *parent = resolveBlock();
712 parent->children.append(std::move(block));
713 m_blockPath.append(parent->children.size() - 1);
714 }
715}
716
717/*!
718 Closes the current block by popping it from the block path.
719
720 Verifies that the inline path depth matches the depth recorded
721 when this block was opened (all formatting pairs balanced).
722 In release builds, the inline path is restored to the expected
723 depth as a safety measure against malformed atom chains.
724*/
725void ContentBuilder::closeBlock()
726{
727 if (!m_blockPath.isEmpty()) {
728 if (Q_UNLIKELY(m_inlineBaseDepths.isEmpty())) {
729 m_inlinePath.clear();
730 m_blockPath.clear();
731 m_inlineBaseDepths.clear();
732 m_inLink = false;
733 return;
734 }
735 const qsizetype expectedDepth = m_inlineBaseDepths.last();
736 if (m_inLink && m_inlinePath.size() > expectedDepth)
737 m_inLink = false;
738 m_inlinePath.resize(expectedDepth);
739 m_inlinePath.resize(expectedDepth);
740 m_inlineBaseDepths.removeLast();
741 m_blockPath.removeLast();
742 }
743}
744
745/*!
746 Adds \a inline_ to the current block's inline content.
747
748 If there is an active inline container (from FormattingLeft or
749 Link atom), the inline is added to that container's children
750 instead.
751
752 If no block is open, the inline is dropped. This shouldn't happen
753 with well-formed atom chains (text is always wrapped in
754 ParaLeft/ParaRight), and is asserted in debug builds.
755*/
756void ContentBuilder::addInline(InlineContent inline_)
757{
758 if (!m_inlinePath.isEmpty()) {
759 resolveInline()->children.append(std::move(inline_));
760 } else if (!m_blockPath.isEmpty()) {
761 resolveBlock()->inlineContent.append(std::move(inline_));
762 } else {
763 if (m_diagnose)
764 m_diagnose(QtWarningMsg, u"Dropping inline content outside any block"_s);
765 }
766}
767
768/*!
769 Convenience method: creates a leaf InlineContent of the given
770 \a type with \a text and appends it via addInline().
771*/
772void ContentBuilder::addLeafInline(InlineType type, const QString &text)
773{
774 InlineContent ic;
775 ic.type = type;
776 ic.text = text;
777 addInline(std::move(ic));
778}
779
780/*!
781 Pushes \a container as a new inline container. Subsequent
782 addInline() and pushInlineContainer() calls will nest
783 their content inside this container.
784
785 Unlike addInline(), which only appends leaf inlines, this method
786 also updates m_inlinePath to enable nesting. It respects the
787 existing inline path: if we are already inside a Link or formatting
788 container, the new container is added as a child of that container.
789*/
790void ContentBuilder::pushInlineContainer(InlineContent container)
791{
792 if (!m_inlinePath.isEmpty()) {
793 auto *parent = resolveInline();
794 parent->children.append(std::move(container));
795 m_inlinePath.append(parent->children.size() - 1);
796 } else if (!m_blockPath.isEmpty()) {
797 auto *block = resolveBlock();
798 block->inlineContent.append(std::move(container));
799 m_inlinePath.append(block->inlineContent.size() - 1);
800 }
801}
802
803/*!
804 Resolves the current block path to a ContentBlock pointer.
805
806 The returned pointer is valid only until the next QList mutation
807 on any list in the path. Callers must use the pointer within a
808 single expression and discard it before appending to any QList.
809*/
810ContentBlock *ContentBuilder::resolveBlock()
811{
812 Q_ASSERT(!m_blockPath.isEmpty());
813 Q_ASSERT(m_blockPath[0] >= 0 && m_blockPath[0] < m_result.size());
814 ContentBlock *block = &m_result[m_blockPath[0]];
815 for (qsizetype i = 1; i < m_blockPath.size(); ++i) {
816 Q_ASSERT(m_blockPath[i] >= 0 && m_blockPath[i] < block->children.size());
817 block = &block->children[m_blockPath[i]];
818 }
819 return block;
820}
821
822/*!
823 Resolves the current inline path to an InlineContent pointer.
824
825 Walks the block path first (via resolveBlock()), then descends
826 through the block's inlineContent and nested children lists
827 using the indices in m_inlinePath.
828*/
829InlineContent *ContentBuilder::resolveInline()
830{
831 ContentBlock *block = resolveBlock();
832 Q_ASSERT(!m_inlinePath.isEmpty());
833 Q_ASSERT(m_inlinePath[0] >= 0 && m_inlinePath[0] < block->inlineContent.size());
834 InlineContent *ic = &block->inlineContent[m_inlinePath[0]];
835 for (qsizetype i = 1; i < m_inlinePath.size(); ++i) {
836 Q_ASSERT(m_inlinePath[i] >= 0 && m_inlinePath[i] < ic->children.size());
837 ic = &ic->children[m_inlinePath[i]];
838 }
839 return ic;
840}
841
842} // namespace IR
843
844QT_END_NAMESPACE
#define ATOM_FORMATTING_TELETYPE
Definition atom.h:215
#define ATOM_FORMATTING_UNDERLINE
Definition atom.h:218
#define ATOM_FORMATTING_NOTRANSLATE
Definition atom.h:210
#define ATOM_LIST_TAG
Definition atom.h:221
#define ATOM_FORMATTING_SUBSCRIPT
Definition atom.h:213
#define ATOM_FORMATTING_BOLD
Definition atom.h:206
#define ATOM_FORMATTING_TRADEMARK
Definition atom.h:216
#define ATOM_LIST_VALUE
Definition atom.h:222
#define ATOM_FORMATTING_ITALIC
Definition atom.h:208
#define ATOM_FORMATTING_LINK
Definition atom.h:209
#define ATOM_FORMATTING_SUPERSCRIPT
Definition atom.h:214
#define ATOM_FORMATTING_INDEX
Definition atom.h:207
#define ATOM_FORMATTING_UICONTROL
Definition atom.h:217
#define ATOM_FORMATTING_PARAMETER
Definition atom.h:211
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:155
@ TableRight
Definition atom.h:97
@ GeneratedList
Definition atom.h:52
@ BriefRight
Definition atom.h:27
@ TableHeaderRight
Definition atom.h:99
@ FormatElse
Definition atom.h:47
@ InlineImage
Definition atom.h:58
@ TableRowRight
Definition atom.h:101
@ Nop
Definition atom.h:74
@ WarningRight
Definition atom.h:111
@ ListTagRight
Definition atom.h:68
@ NavLink
Definition atom.h:73
@ ListItemNumber
Definition atom.h:66
@ SinceTagRight
Definition atom.h:91
@ CodeBad
Definition atom.h:32
@ AnnotatedList
Definition atom.h:22
@ SectionRight
Definition atom.h:84
@ SectionHeadingLeft
Definition atom.h:85
@ TableLeft
Definition atom.h:96
@ ListItemRight
Definition atom.h:70
@ Image
Definition atom.h:54
@ TableItemRight
Definition atom.h:103
@ ListItemLeft
Definition atom.h:69
@ Code
Definition atom.h:31
@ ListLeft
Definition atom.h:65
@ NavAutoLink
Definition atom.h:72
@ BriefLeft
Definition atom.h:26
@ ImageText
Definition atom.h:55
@ ListRight
Definition atom.h:71
@ ParaRight
Definition atom.h:78
@ Qml
Definition atom.h:79
@ FormattingLeft
Definition atom.h:50
@ FormattingRight
Definition atom.h:51
@ SectionHeadingRight
Definition atom.h:86
@ Link
Definition atom.h:63
@ FormatEndif
Definition atom.h:48
@ SinceTagLeft
Definition atom.h:90
@ AutoLink
Definition atom.h:23
@ TableItemLeft
Definition atom.h:102
@ NoteRight
Definition atom.h:76
@ BaseName
Definition atom.h:24
@ FormatIf
Definition atom.h:49
virtual bool isLinkAtom() const
Definition atom.h:163
const Atom * next() const
Return the next atom in the atom list.
Definition atom.h:152
virtual Genus genus()
Definition atom.h:164
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 ...
ContentBuilder(BriefHandling briefHandling=BriefHandling::Skip, int headingOffset=0, DiagnosticHandler diagnosticHandler={})
Constructs a ContentBuilder.
static QString genusToString(Genus genus)
static QString stripCodeMarkerTags(const QString &markedCode)
static QString placeholderSortAttribute(const Atom *atom)
Definition builder.cpp:14
LinkState
LinkOrigin
BriefHandling
BlockType
static InlineType formattingToInlineType(const QString &formatting)
InlineType
Combined button and popup list for selecting options.
Represents a structural block element in documentation.
LinkData(LinkOrigin o, LinkState s, std::optional< SourceLocation > loc=std::nullopt)
Represents inline content within a documentation block.