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 if (atom->isLinkAtom()) {
356 // genus() and domain() are non-const but non-mutating;
357 // the same const_cast pattern is used in qdocdatabase.cpp.
358 auto *mutableAtom = const_cast<Atom *>(atom);
359 Genus genus = mutableAtom->genus();
360 QString genusStr = genusToString(genus);
361 if (!genusStr.isEmpty())
362 link.attributes["linkGenus"_L1] = genusStr;
363 if (Tree *domain = mutableAtom->domain())
364 link.attributes["linkModule"_L1] = domain->physicalModuleName();
365 if (int flags = mutableAtom->flags())
366 link.attributes["linkFlags"_L1] = flags;
367
368 const auto *linkAtom = static_cast<const LinkAtom *>(mutableAtom);
369 link.link->sourceLocation = InlineContent::SourceLocation{
370 linkAtom->location.filePath(),
371 linkAtom->location.lineNo()
372 };
373 }
374
375 pushInlineContainer(std::move(link));
376
377 // Link atoms are always followed by FormattingLeft("link");
378 // skip it to avoid double-processing.
380 && atom->next()->string() == ATOM_FORMATTING_LINK) {
381 return atom->next();
382 }
383 break;
384 }
385
387 const QString &fmt = atom->string();
388
389 if (fmt == ATOM_FORMATTING_INDEX || fmt.startsWith(u"span "_s))
390 break;
391
392 if (fmt == ATOM_FORMATTING_LINK)
393 break;
394
396 break;
397
398 if (fmt == ATOM_FORMATTING_UICONTROL) {
399 InlineContent bold;
401 pushInlineContainer(std::move(bold));
402 break;
403 }
404
405 InlineType type = formattingToInlineType(fmt);
406 if (type == InlineType::Text)
407 break;
408
409 InlineContent container;
410 container.type = type;
411 pushInlineContainer(std::move(container));
412 break;
413 }
414
416 const QString &fmt = atom->string();
417
418 if (fmt == ATOM_FORMATTING_LINK) {
419 if (m_inLink) {
420 const qsizetype base = m_inlineBaseDepths.isEmpty() ? 0 : m_inlineBaseDepths.last();
421 if (m_inlinePath.size() > base) {
422 Q_ASSERT(resolveInline()->type == InlineType::Link);
423 m_inlinePath.removeLast();
424 }
425 m_inLink = false;
426 }
427 break;
428 }
429
431 || fmt.startsWith(u"span "_s) || fmt == ATOM_FORMATTING_TRADEMARK) {
432 break;
433 }
434
435 const qsizetype base = m_inlineBaseDepths.isEmpty() ? 0 : m_inlineBaseDepths.last();
436 if (m_inlinePath.size() > base)
437 m_inlinePath.removeLast();
438 break;
439 }
440
441 case Atom::SectionLeft:
442 openBlock(BlockType::Section);
443 break;
444
446 closeBlock();
447 break;
448
450 QJsonObject attrs;
451 attrs["level"_L1] = atom->string().toInt() + m_headingOffset;
452 // SectionHeadingLeft carries the heading level at string(0) and the
453 // anchor identifier at string(1). The second payload is populated
454 // only when a heading-level macro (\sectionN) emits an explicit ref.
455 if (atom->count() >= 2)
456 attrs["sectionRef"_L1] = atom->string(1);
457 openBlock(BlockType::SectionHeading, attrs);
458 break;
459 }
460
462 closeBlock();
463 break;
464
465 case Atom::ListLeft: {
466 QJsonObject attrs;
467 const QString &listType = atom->string();
468 attrs["listType"_L1] = listType;
469 if (listType == ATOM_LIST_TAG || listType == ATOM_LIST_VALUE)
470 openBlock(BlockType::DefinitionList, attrs);
471 else
472 openBlock(BlockType::List, attrs);
473 break;
474 }
475
476 case Atom::ListRight:
477 closeBlock();
478 break;
479
480 case Atom::ListItemLeft: {
481 ContentBlock *parent = resolveBlock();
482 if (parent->type == BlockType::DefinitionList)
483 openBlock(BlockType::DefinitionDescription);
484 else
485 openBlock(BlockType::ListItem);
486 break;
487 }
488
490 closeBlock();
491 break;
492
494 // Start-number metadata is not yet represented in the IR.
495 break;
496
497 case Atom::NoteLeft:
498 openBlock(BlockType::Note);
499 break;
500
501 case Atom::NoteRight:
502 closeBlock();
503 break;
504
505 case Atom::WarningLeft:
506 openBlock(BlockType::Warning);
507 break;
508
510 closeBlock();
511 break;
512
513 case Atom::ImportantLeft:
514 openBlock(BlockType::Important);
515 break;
516
518 closeBlock();
519 break;
520
521 case Atom::BR:
522 addLeafInline(InlineType::LineBreak, {});
523 break;
524
525 case Atom::HR:
526 openBlock(BlockType::HorizontalRule);
527 closeBlock();
528 break;
529
530 case Atom::AnnotatedList: {
531 QJsonObject attrs;
532 attrs["variant"_L1] = toString(ListPlaceholderVariant::AnnotatedGroup);
533 attrs["argument"_L1] = atom->string();
534 attrs["sort"_L1] = placeholderSortAttribute(atom);
535 openBlock(BlockType::ListPlaceholder, attrs);
536 closeBlock();
537 break;
538 }
539
540 case Atom::GeneratedList: {
541 const QString arg = atom->string();
542 QJsonObject attrs;
543 attrs["argument"_L1] = arg;
544 attrs["sort"_L1] = placeholderSortAttribute(atom);
545
546 static constexpr auto classesPrefix = "classes "_L1;
547 if (arg == "annotatedexamples"_L1) {
548 attrs["variant"_L1] = toString(ListPlaceholderVariant::AnnotatedExamples);
549 } else if (arg == "annotatedclasses"_L1) {
550 attrs["variant"_L1] = toString(ListPlaceholderVariant::AnnotatedClasses);
551 } else if (arg == "classes"_L1 || arg.startsWith(classesPrefix)) {
552 attrs["variant"_L1] = toString(ListPlaceholderVariant::CompactClasses);
553 if (arg.startsWith(classesPrefix)) {
554 const QString rootName = arg.sliced(classesPrefix.size()).trimmed();
555 if (!rootName.isEmpty())
556 attrs["rootName"_L1] = rootName;
557 }
558 } else {
559 // Unknown \generatelist variant: keep the legacy attribute-bag
560 // Div so deferred variants continue to render as they do today.
561 // Remove this branch when those variants acquire their own
562 // ListPlaceholderVariant entries in a follow-up phase.
563 QJsonObject fallback;
564 fallback["generatedList"_L1] = arg;
565 openBlock(BlockType::Div, fallback);
566 closeBlock();
567 break;
568 }
569
570 openBlock(BlockType::ListPlaceholder, attrs);
571 closeBlock();
572 break;
573 }
574
575 case Atom::TableLeft: {
576 QJsonObject attrs;
577 QString tableStyle = u"generic"_s;
578 QString width;
579
580 for (int i = 0; i < atom->count(); ++i) {
581 const QString &arg = atom->string(i);
582 if (arg == "borderless"_L1)
583 tableStyle = arg;
584 else if (arg.contains('%'_L1))
585 width = arg;
586 }
587
588 // Handle "100 %" (space before percent) — the percent arrives
589 // as a separate atom argument, reconstruct the width value.
590 if (width == "%"_L1) {
591 bool ok = false;
592 int pct = atom->string(0).toInt(&ok);
593 width = ok ? QString::number(pct) + '%'_L1 : QString();
594 }
595
596 attrs["style"_L1] = tableStyle;
597 if (!width.isEmpty())
598 attrs["width"_L1] = width;
599 openBlock(BlockType::Table, attrs);
600 break;
601 }
602
603 case Atom::TableRight:
604 closeBlock();
605 break;
606
607 case Atom::TableHeaderLeft:
608 openBlock(BlockType::TableHeaderRow);
609 break;
610
612 closeBlock();
613 break;
614
615 case Atom::TableRowLeft:
616 openBlock(BlockType::TableRow);
617 break;
618
620 closeBlock();
621 break;
622
623 case Atom::TableItemLeft: {
624 QJsonObject attrs;
625 const QString &spec = atom->string();
626 if (!spec.isEmpty()) {
627 const auto parts = QStringView{spec}.split(u',');
628 if (parts.size() >= 2) {
629 int colspan = qMax(1, parts[0].toInt());
630 int rowspan = qMax(1, parts[1].toInt());
631 if (colspan > 1)
632 attrs["colspan"_L1] = colspan;
633 if (rowspan > 1)
634 attrs["rowspan"_L1] = rowspan;
635 }
636 }
637 openBlock(BlockType::TableCell, attrs);
638 break;
639 }
640
642 closeBlock();
643 break;
644
645 case Atom::ListTagLeft:
646 openBlock(BlockType::DefinitionTerm);
647 break;
648
650 closeBlock();
651 break;
652
653 case Atom::Image: {
654 QJsonObject attrs;
655 attrs["class"_L1] = u"centerAlign"_s;
656 openBlock(BlockType::Paragraph, attrs);
657
658 InlineContent img;
660 img.href = atom->string();
661 if (atom->next() && atom->next()->type() == Atom::ImageText)
662 img.title = atom->next()->string();
663 addInline(std::move(img));
664
665 closeBlock();
666
667 if (atom->next() && atom->next()->type() == Atom::ImageText)
668 return atom->next();
669 break;
670 }
671
672 case Atom::InlineImage: {
673 if (Q_UNLIKELY(m_blockPath.isEmpty()))
674 break;
675
676 InlineContent img;
678 img.href = atom->string();
679 if (atom->next() && atom->next()->type() == Atom::ImageText)
680 img.title = atom->next()->string();
681 addInline(std::move(img));
682
683 if (atom->next() && atom->next()->type() == Atom::ImageText)
684 return atom->next();
685 break;
686 }
687
688 case Atom::ImageText:
689 break;
690
691 case Atom::CaptionLeft: {
692 QJsonObject attrs;
693 attrs["class"_L1] = u"figCaption"_s;
694 openBlock(BlockType::Paragraph, attrs);
695 break;
696 }
697
699 closeBlock();
700 break;
701
704 break;
705
706 case Atom::Nop:
707 case Atom::BaseName:
708 break;
709
710 default:
711 break;
712 }
713 return atom;
714}
715
716/*!
717 Opens a new block of type \a type with optional \a attrs.
718
719 If the block path is empty, the block is added to the top-level
720 result list. Otherwise it is added as a child of the current
721 container block.
722*/
723void ContentBuilder::openBlock(BlockType type, QJsonObject attrs)
724{
725 ContentBlock block;
726 block.type = type;
727 block.attributes = std::move(attrs);
728
729 m_inlineBaseDepths.append(m_inlinePath.size());
730
731 if (m_blockPath.isEmpty()) {
732 m_result.append(std::move(block));
733 m_blockPath.append(m_result.size() - 1);
734 } else {
735 auto *parent = resolveBlock();
736 parent->children.append(std::move(block));
737 m_blockPath.append(parent->children.size() - 1);
738 }
739}
740
741/*!
742 Closes the current block by popping it from the block path.
743
744 Verifies that the inline path depth matches the depth recorded
745 when this block was opened (all formatting pairs balanced).
746 In release builds, the inline path is restored to the expected
747 depth as a safety measure against malformed atom chains.
748*/
749void ContentBuilder::closeBlock()
750{
751 if (!m_blockPath.isEmpty()) {
752 if (Q_UNLIKELY(m_inlineBaseDepths.isEmpty())) {
753 m_inlinePath.clear();
754 m_blockPath.clear();
755 m_inlineBaseDepths.clear();
756 m_inLink = false;
757 return;
758 }
759 const qsizetype expectedDepth = m_inlineBaseDepths.last();
760 if (m_inLink && m_inlinePath.size() > expectedDepth)
761 m_inLink = false;
762 m_inlinePath.resize(expectedDepth);
763 m_inlinePath.resize(expectedDepth);
764 m_inlineBaseDepths.removeLast();
765 m_blockPath.removeLast();
766 }
767}
768
769/*!
770 Adds \a inline_ to the current block's inline content.
771
772 If there is an active inline container (from FormattingLeft or
773 Link atom), the inline is added to that container's children
774 instead.
775
776 If no block is open, the inline is dropped. This shouldn't happen
777 with well-formed atom chains (text is always wrapped in
778 ParaLeft/ParaRight), and is asserted in debug builds.
779*/
780void ContentBuilder::addInline(InlineContent inline_)
781{
782 if (!m_inlinePath.isEmpty()) {
783 resolveInline()->children.append(std::move(inline_));
784 } else if (!m_blockPath.isEmpty()) {
785 resolveBlock()->inlineContent.append(std::move(inline_));
786 } else {
787 if (m_diagnose)
788 m_diagnose(QtWarningMsg, u"Dropping inline content outside any block"_s);
789 }
790}
791
792/*!
793 Convenience method: creates a leaf InlineContent of the given
794 \a type with \a text and appends it via addInline().
795*/
796void ContentBuilder::addLeafInline(InlineType type, const QString &text)
797{
798 InlineContent ic;
799 ic.type = type;
800 ic.text = text;
801 addInline(std::move(ic));
802}
803
804/*!
805 Pushes \a container as a new inline container. Subsequent
806 addInline() and pushInlineContainer() calls will nest
807 their content inside this container.
808
809 Unlike addInline(), which only appends leaf inlines, this method
810 also updates m_inlinePath to enable nesting. It respects the
811 existing inline path: if we are already inside a Link or formatting
812 container, the new container is added as a child of that container.
813*/
814void ContentBuilder::pushInlineContainer(InlineContent container)
815{
816 if (!m_inlinePath.isEmpty()) {
817 auto *parent = resolveInline();
818 parent->children.append(std::move(container));
819 m_inlinePath.append(parent->children.size() - 1);
820 } else if (!m_blockPath.isEmpty()) {
821 auto *block = resolveBlock();
822 block->inlineContent.append(std::move(container));
823 m_inlinePath.append(block->inlineContent.size() - 1);
824 }
825}
826
827/*!
828 Resolves the current block path to a ContentBlock pointer.
829
830 The returned pointer is valid only until the next QList mutation
831 on any list in the path. Callers must use the pointer within a
832 single expression and discard it before appending to any QList.
833*/
834ContentBlock *ContentBuilder::resolveBlock()
835{
836 Q_ASSERT(!m_blockPath.isEmpty());
837 Q_ASSERT(m_blockPath[0] >= 0 && m_blockPath[0] < m_result.size());
838 ContentBlock *block = &m_result[m_blockPath[0]];
839 for (qsizetype i = 1; i < m_blockPath.size(); ++i) {
840 Q_ASSERT(m_blockPath[i] >= 0 && m_blockPath[i] < block->children.size());
841 block = &block->children[m_blockPath[i]];
842 }
843 return block;
844}
845
846/*!
847 Resolves the current inline path to an InlineContent pointer.
848
849 Walks the block path first (via resolveBlock()), then descends
850 through the block's inlineContent and nested children lists
851 using the indices in m_inlinePath.
852*/
853InlineContent *ContentBuilder::resolveInline()
854{
855 ContentBlock *block = resolveBlock();
856 Q_ASSERT(!m_inlinePath.isEmpty());
857 Q_ASSERT(m_inlinePath[0] >= 0 && m_inlinePath[0] < block->inlineContent.size());
858 InlineContent *ic = &block->inlineContent[m_inlinePath[0]];
859 for (qsizetype i = 1; i < m_inlinePath.size(); ++i) {
860 Q_ASSERT(m_inlinePath[i] >= 0 && m_inlinePath[i] < ic->children.size());
861 ic = &ic->children[m_inlinePath[i]];
862 }
863 return ic;
864}
865
866} // namespace IR
867
868QT_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.