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