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
templategenerator.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 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 "aggregate.h"
8#include "codemarker.h"
10#include "config.h"
13#include "documentwriter.h"
14#include "generator.h"
15#include "hrefresolver.h"
16#include "injabridge.h"
17#include "ir/builder.h"
18#include "ir/document.h"
19#include "ir/listexpander.h"
20#include "linkresolver.h"
21#include "node.h"
22#include "nodeextractor.h"
23#include "outputcontext.h"
25#include "pagenode.h"
26#include "qdocdatabase.h"
27#include "qmltypenode.h"
28#include "sections.h"
29#include "tree.h"
30#include "utilities.h"
31
32#include <optional>
33#include <utility>
34#include <variant>
35
36#include <QtCore/qdir.h>
37#include <QtCore/qdiriterator.h>
38#include <QtCore/qfile.h>
39#include <QtCore/qfileinfo.h>
40#include <QtCore/qloggingcategory.h>
41#include <QtCore/qtextstream.h>
42
44
45Q_LOGGING_CATEGORY(lcQDocTemplateGenerator, "qt.qdoc.templategenerator")
46
47using namespace Qt::Literals;
48
49static void processDocumentBlocks(IR::ListExpander *expander, LinkResolver *resolver,
50 IR::Document &ir, const Node *page,
51 const NodeExtractor::SemanticOrigins *origins = nullptr);
52
58
59static std::optional<LoadedTemplate> loadTemplate(const QString &templateDir, const QString &name)
60{
61 auto read = [](QString path) -> std::optional<LoadedTemplate> {
62 QFile file(path);
63 if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
64 return std::nullopt;
65 return LoadedTemplate{ std::move(path), QString::fromUtf8(file.readAll()) };
66 };
67
68 if (!templateDir.isEmpty()) {
69 auto loaded = read(templateDir + '/'_L1 + name);
70 if (loaded)
71 return loaded;
72 }
73
74 return read(":/qdoc/templates/"_L1 + name);
75}
76
77static void writeOrDiscardRenderResult(FileDocumentWriter *writer,
78 const InjaBridge::RenderContext &context,
79 const InjaBridge::RenderResult &result,
80 const TemplateGenerator::RenderFailureHandler &reportFailure)
81{
82 if (const auto *failure = std::get_if<InjaBridge::RenderFailure>(&result)) {
83 reportFailure(InjaBridge::renderErrorText(context, failure->message));
84 if (writer)
85 writer->discardDocument();
86 } else if (writer && writer->isOpen()) {
87 writer->write(std::get<QString>(result));
88 }
89}
90
91/*!
92 \class TemplateGenerator
93 \internal
94 \brief Generates documentation using external templates and a pre-built IR.
95
96 TemplateGenerator implements OutputProducer and DocumentationHandler to
97 generate documentation without inheriting from Generator. It uses
98 DocumentationTraverser for tree traversal and delegates content generation
99 to templates via the IR system.
100
101 \section1 Architecture
102
103 The generator follows a composition-based design:
104 \list
105 \li \b{OutputProducer}: Lifecycle interface (prepare/produce/finalize).
106 \li \b{DocumentationHandler}: Content generation callbacks for
107 traverser.
108 \li \b{DocumentationTraverser}: Shared tree traversal logic.
109 \li \b{DocumentWriter}: Output abstraction (file or string for tests).
110 \endlist
111
112 \sa DocumentationTraverser, DocumentationHandler, OutputProducer,
113 IR::Builder
114*/
115
116/*!
117 Constructs a producer for \a format using \a fileResolver and \a qdb.
118 The required \a reportFailure callback reports a render diagnostic and
119 records that the QDoc run must exit non-zero.
120*/
122 const QString &format, RenderFailureHandler reportFailure)
123 : m_fileResolver(fileResolver)
124 , m_qdb(qdb)
125 , m_format(format.isEmpty() ? u"template"_s : format)
127{
129}
130
135
137{
138 createDefaultWriter();
139
140 const Config &config = Config::instance();
141
142 QString extensionConfig = config.get(m_format + ".extension"_L1).asString();
143 if (!extensionConfig.isEmpty())
144 m_fileExtension = extensionConfig;
145
146 // Mirror the finalized extension into OutputContext. fromConfig()
147 // can't set it because the extension is a property of the generator,
148 // not the qdocconf.
149 m_context->fileExtension = m_fileExtension;
150
151 const ConfigVar &templateDirVar = config.get(m_format + ".templatedir"_L1);
152 QString templateDirConfig = templateDirVar.asString();
153
154 if (templateDirConfig.isEmpty()) {
155 m_templateDir.clear();
156 } else if (QDir::isAbsolutePath(templateDirConfig)) {
157 m_templateDir = templateDirConfig;
158 } else {
159 // Resolve relative to the .qdocconf file that declared the variable,
160 // not the outermost .qdocconf. Reaching the template generator's
161 // config through an include() chain is the common case: qtbase's
162 // qt-module-defaults.qdocconf includes qt-template-generator.qdocconf,
163 // which in turn sets `TemplateHTML.templatedir = template/qt-branded`.
164 // Without per-variable path tracking, that relative path would
165 // resolve against whichever module's top-level qdocconf is being
166 // processed — a different directory per module — and the template
167 // set would never be found. Falling back to the legacy currentDir()
168 // base preserves behaviour for programmatic insertion, where the
169 // variable has no declaring file.
170 const QString base = templateDirVar.path().isEmpty()
171 ? config.currentDir()
172 : templateDirVar.path();
173 m_templateDir = QDir::cleanPath(QDir(base).absoluteFilePath(templateDirConfig));
174 }
175
176 bool foundTemplates = false;
177 if (!m_templateDir.isEmpty()) {
178 QDir templateDir(m_templateDir);
179 if (templateDir.exists() && !templateDir.entryList(QDir::Files).isEmpty()) {
180 foundTemplates = true;
181 qCDebug(lcQDocTemplateGenerator) << "[%1]"_L1.arg(m_format) << "Using template directory:" << m_templateDir;
182 } else if (!templateDir.exists()) {
183 qCInfo(lcQDocTemplateGenerator)
184 << "[%1]"_L1.arg(m_format) << "Configured template directory does not exist:" << m_templateDir
185 << "- will use embedded templates";
186 } else {
187 qCInfo(lcQDocTemplateGenerator)
188 << "[%1]"_L1.arg(m_format) << "Configured template directory is empty:" << m_templateDir
189 << "- will use embedded templates";
190 }
191 } else {
192 qCDebug(lcQDocTemplateGenerator)
193 << "[%1]"_L1.arg(m_format) << "No external template directory configured - will use embedded templates";
194 }
195
196 if (!foundTemplates)
197 m_templateDir.clear();
198
199 m_emitStylesheet = config.get(m_format + ".stylesheet"_L1).asBool();
200
201 m_stylesheetName = config.get(m_format + ".stylesheetname"_L1).asString();
202 if (m_stylesheetName.isEmpty())
203 m_stylesheetName = u"qdoc-default.css"_s;
204
205 // Enforce plain filename — no directory separators, no traversal.
206 // The stylesheet is copied to and referenced from the output root,
207 // so subpaths would create inconsistencies between theme-provided
208 // assets and the QRC fallback.
209 if (m_stylesheetName.contains('/'_L1)
210 || m_stylesheetName.contains('\\'_L1)
211 || m_stylesheetName.contains(".."_L1)
212 || QDir::isAbsolutePath(m_stylesheetName)) {
213 qCWarning(lcQDocTemplateGenerator)
214 << "[%1]"_L1.arg(m_format) << "Ignoring stylesheetname:" << m_stylesheetName
215 << "— must be a plain filename (no path separators)";
216 m_stylesheetName = u"qdoc-default.css"_s;
217 }
218
219 copyAssets();
220
221 HrefResolverConfig hrefConfig;
222 hrefConfig.context = &*m_context;
223 hrefConfig.inclusionPolicy = config.createInclusionPolicy();
224 hrefConfig.cleanRefFn = [](const QString &ref) { return Generator::cleanRef(ref); };
225 hrefConfig.qmlTypeContextFn = []() -> const QmlTypeNode * {
227 };
228 m_hrefResolver = std::make_unique<HrefResolver>(hrefConfig);
229
230 LinkResolverConfig linkConfig;
231 linkConfig.autolinkErrors = Generator::autolinkErrors();
232 linkConfig.noLinkErrors = Generator::noLinkErrors();
233 m_linkResolver = std::make_unique<LinkResolver>(&m_qdb, *m_hrefResolver, linkConfig);
234
235 m_catalogSource = std::make_unique<CatalogEntrySource>(
236 m_qdb, *m_hrefResolver, config.createInclusionPolicy());
237
238 IR::ListExpanderCallbacks callbacks;
239 callbacks.collectCppClasses =
240 [this](const Node *relative, Qt::SortOrder sortOrder) {
241 return m_catalogSource->collectCppClasses(relative, sortOrder);
242 };
243 callbacks.collectExamplesGrouped =
244 [this](const Node *relative) {
245 return m_catalogSource->collectExamplesGrouped(relative);
246 };
247 callbacks.collectCompactClasses =
248 [this](const Node *relative, const QString &rootName) {
249 return m_catalogSource->collectCompactClasses(relative, rootName);
250 };
251 callbacks.collectGroupMembers =
252 [this](const Node *relative, const QString &groupName,
253 Qt::SortOrder sortOrder) {
254 return m_catalogSource->collectGroupMembers(
255 relative, groupName, sortOrder);
256 };
257 callbacks.onEmpty =
258 [](const QString &argument, IR::ListPlaceholderVariant variant) {
259 qCWarning(lcQDocTemplateGenerator)
260 << "\\generatelist or \\annotatedlist with argument"
261 << argument << "(variant"
262 << IR::toString(variant) << ")"
263 << "expanded to no entries; the catalog renders"
264 << "as empty.";
265 };
266 m_listExpander = std::make_unique<IR::ListExpander>(std::move(callbacks));
267}
268
270{
271 DocumentationTraverser traverser;
272 Node *root = m_qdb.primaryTreeRoot();
273 if (root)
274 traverser.traverse(root, *this);
275}
276
278{
279 m_writer.reset();
280}
281
283{
284 return m_format;
285}
286
287void TemplateGenerator::beginDocument(const QString &outputFileName)
288{
289 m_documentNames.append(outputFileName);
290 if (m_writer)
291 m_writer->beginDocument(outputFileName);
292}
293
295{
296 m_documentNames.removeLast();
297 if (m_writer)
298 m_writer->endDocument();
299}
300
301/*!
302 \internal
303
304 Returns the output page name recorded by the most recent beginDocument()
305 that has not yet ended, or QString() outside a document. Unlike the
306 writer's current file name, it survives the writer failing to open
307 the file, so a render error still names the attempted page.
308*/
309QString TemplateGenerator::currentDocumentName() const
310{
311 return m_documentNames.isEmpty() ? QString() : m_documentNames.constLast();
312}
313
315{
316 if (!node->url().isEmpty())
317 return node->url();
318
319 // Special case for simple page nodes (\page commands) with explicit
320 // non-.html extensions. Use the normalized fileBase() but preserve
321 // user specified extension
323 QFileInfo originalName(node->name());
324 QString suffix = originalName.suffix();
325 if (!suffix.isEmpty() && suffix != "html"_L1) {
326 // User specified a non-.html extension - use normalized base + original extension
327 QString name = fileBase(node);
328 return name + '.'_L1 + suffix;
329 }
330 }
331
332 QString name = fileBase(node) + '.'_L1;
333 return name + m_fileExtension;
334}
335
337{
338 Q_UNUSED(marker);
339
340 IR::PageMetadata pm = NodeExtractor::extractPageMetadata(cn, m_hrefResolver.get());
341
342 IR::Builder builder;
343 IR::Document ir = builder.buildPageIR(std::move(pm));
344
345 processDocumentBlocks(m_listExpander.get(), m_linkResolver.get(), ir, cn);
346
347 resolveImagePaths(ir);
348 renderDocument(ir, "collection"_L1);
349}
350
352{
353 Q_UNUSED(marker);
354
355 IR::PageMetadata pm = NodeExtractor::extractPageMetadata(cn, m_hrefResolver.get());
356
357 IR::Builder builder;
358 IR::Document ir = builder.buildPageIR(std::move(pm));
359
360 processDocumentBlocks(m_listExpander.get(), m_linkResolver.get(), ir, cn);
361
362 resolveImagePaths(ir);
363 renderDocument(ir, "collection"_L1);
364}
365
367{
368 Q_UNUSED(marker);
369
370 IR::PageMetadata pm = NodeExtractor::extractPageMetadata(pn, m_hrefResolver.get());
371
372 IR::Builder builder;
373 IR::Document ir = builder.buildPageIR(std::move(pm));
374
375 processDocumentBlocks(m_listExpander.get(), m_linkResolver.get(), ir, pn);
376
377 resolveImagePaths(ir);
378 renderDocument(ir, "page"_L1);
379}
380
382{
383 Q_UNUSED(marker);
384
385 // Page-local, never serialized: the origin paired with each emitted
386 // detail member, for link resolution in the authored member context.
387 NodeExtractor::SemanticOrigins origins;
388 IR::PageMetadata pm = NodeExtractor::extractPageMetadata(aggregate, m_hrefResolver.get(),
389 &origins);
390 auto allMembers = NodeExtractor::extractAllMembersIR(aggregate, m_hrefResolver.get());
391
392 IR::Builder builder;
393 IR::Document ir = builder.buildPageIR(std::move(pm));
394
395 if (allMembers)
396 ir.membersPageUrl = fileBase(aggregate) + "-members."_L1 + m_fileExtension;
397
398 if (ir.cppReferenceInfo && ir.cppReferenceInfo->hasObsoleteMembers)
399 ir.cppReferenceInfo->obsoleteMembersUrl =
400 fileBase(aggregate) + "-obsolete."_L1 + m_fileExtension;
401
402 processDocumentBlocks(m_listExpander.get(), m_linkResolver.get(), ir, aggregate, &origins);
403
404 resolveImagePaths(ir);
405 renderDocument(ir, "cppref"_L1);
406
407 if (allMembers)
408 generateMemberListingPage(aggregate, *allMembers);
409
410 if (ir.cppReferenceInfo && ir.cppReferenceInfo->hasObsoleteMembers)
411 generateObsoleteMembersPage(aggregate);
412}
413
415{
416 Q_UNUSED(marker);
417
418 // Page-local, never serialized: the origin paired with each emitted
419 // detail member, for link resolution in the authored member context.
420 NodeExtractor::SemanticOrigins origins;
421 IR::PageMetadata pm = NodeExtractor::extractPageMetadata(qcn, m_hrefResolver.get(),
422 &origins);
423 auto allMembers = NodeExtractor::extractAllMembersIR(qcn, m_hrefResolver.get());
424
425 IR::Builder builder;
426 IR::Document ir = builder.buildPageIR(std::move(pm));
427
428 if (allMembers)
429 ir.membersPageUrl = fileBase(qcn) + "-members."_L1 + m_fileExtension;
430
431 if (ir.qmlTypeInfo && ir.qmlTypeInfo->hasObsoleteMembers)
432 ir.qmlTypeInfo->obsoleteMembersUrl =
433 fileBase(qcn) + "-obsolete."_L1 + m_fileExtension;
434
435 processDocumentBlocks(m_listExpander.get(), m_linkResolver.get(), ir, qcn, &origins);
436
437 resolveImagePaths(ir);
438 renderDocument(ir, "qmltype"_L1);
439
440 if (allMembers)
441 generateMemberListingPage(qcn, *allMembers);
442
443 if (ir.qmlTypeInfo && ir.qmlTypeInfo->hasObsoleteMembers)
444 generateObsoleteMembersPage(qcn);
445}
446
448{
449 Q_UNUSED(marker);
450
451 // Placeholder - IR integration pending
452 if (m_writer && m_writer->isOpen()) {
453 m_writer->writeLine(QString(u"<!-- TemplateGenerator: Proxy Page for "_s
454 + aggregate->name() + u" -->"_s));
455 m_writer->writeLine(QString(u"<h1>"_s + aggregate->fullTitle() + u"</h1>"_s));
456 m_writer->writeLine(u"<p>Template-based output (IR integration pending)</p>"_s);
457 }
458}
459
464
466{
467 return m_fileExtension;
468}
469
470/*!
471 \internal
472 Render phase: Format pre-built IR according to a template.
473
474 This is TemplateGenerator's core responsibility. It receives IR and
475 produces formatted output without any knowledge of Nodes or the database.
476*/
477void TemplateGenerator::renderDocument(const IR::Document &ir, const QString &templateBaseName)
478{
479 const QString templateFileName = templateBaseName + '.'_L1 + m_fileExtension;
480 const auto loaded = loadTemplate(m_templateDir, templateFileName);
481
482 if (!loaded)
483 qFatal("TemplateGenerator[%s]: No template file found for extension '%s'. "
484 "Ensure '%s.%s' exists in the configured template directory or in resources.",
485 qPrintable(m_format), qPrintable(m_fileExtension),
486 qPrintable(templateBaseName), qPrintable(m_fileExtension));
487
488 QJsonObject json = ir.toJson();
489 json["stylesheetEnabled"_L1] = m_emitStylesheet;
490 json["stylesheetName"_L1] = m_stylesheetName;
491
492 const InjaBridge::RenderContext context{ m_format, currentDocumentName(), loaded->path };
493
494 auto includeCallback = [this](const QString &name) { return resolveInclude(name); };
495 const auto result = InjaBridge::render(loaded->content, json, includeCallback, context);
496 writeOrDiscardRenderResult(m_writer.get(), context, result, m_reportFailure);
497}
498
499/*!
500 \internal
501 Render a raw QJsonObject through a named template.
502
503 Unlike renderDocument(), this takes an arbitrary JSON object rather
504 than an IR::Document. It's used for sub-pages (such as the member
505 listing page) where the data structure differs from Document's.
506*/
507void TemplateGenerator::renderJson(const QJsonObject &json, const QString &templateBaseName)
508{
509 const QString templateFileName = templateBaseName + '.'_L1 + m_fileExtension;
510 const auto loaded = loadTemplate(m_templateDir, templateFileName);
511
512 if (!loaded)
513 qFatal("TemplateGenerator[%s]: No template file found for '%s'. "
514 "Ensure '%s.%s' exists in the configured template directory or in resources.",
515 qPrintable(m_format), qPrintable(templateBaseName),
516 qPrintable(templateBaseName), qPrintable(m_fileExtension));
517
518 QJsonObject enrichedJson = json;
519 enrichedJson["stylesheetEnabled"_L1] = m_emitStylesheet;
520 enrichedJson["stylesheetName"_L1] = m_stylesheetName;
521 if (!enrichedJson.contains("hasNavigation"_L1))
522 enrichedJson["hasNavigation"_L1] = false;
523
524 const InjaBridge::RenderContext context{ m_format, currentDocumentName(), loaded->path };
525
526 auto includeCallback = [this](const QString &name) { return resolveInclude(name); };
527 const auto result = InjaBridge::render(loaded->content, enrichedJson, includeCallback, context);
528 writeOrDiscardRenderResult(m_writer.get(), context, result, m_reportFailure);
529}
530
531/*!
532 \internal
533 Generate a member listing sub-page for a C++ class or QML type.
534
535 Opens a new output file, renders the all-members data through the
536 \c members template, and closes the file. This mirrors
537 HtmlGenerator::generateAllMembersFile() but uses the IR pipeline
538 and template system instead of inline HTML generation.
539*/
540void TemplateGenerator::generateMemberListingPage(const Node *node,
541 const IR::AllMembersIR &allMembers)
542{
543 const QString membersFileName =
544 fileBase(node) + "-members."_L1 + m_fileExtension;
545
546 QJsonObject json = allMembers.toJson();
547 json["title"_L1] = QString("List of All Members for "_L1 + allMembers.typeName);
548
549 beginDocument(membersFileName);
550 renderJson(json, "members"_L1);
552}
553
554/*!
555 \internal
556 Generate a sub-page listing obsolete members for a C++ aggregate.
557
558 Opens a new output file, extracts obsolete summary and detail
559 sections through the Sections API, converts each member to MemberIR
560 JSON, and renders the result through the \c obsolete template.
561*/
562void TemplateGenerator::generateObsoleteMembersPage(const Aggregate *aggregate)
563{
564 Sections sections(aggregate);
565 SectionPtrVector obsoleteSummary;
566 SectionPtrVector obsoleteDetail;
567
568 if (!sections.hasObsoleteMembers(&obsoleteSummary, &obsoleteDetail))
569 return;
570
571 const QString obsoleteFileName =
572 fileBase(aggregate) + "-obsolete."_L1 + m_fileExtension;
573
574 QJsonObject json;
575 json["title"_L1] = QString("Obsolete Members for "_L1 + aggregate->plainFullName());
576 json["typeName"_L1] = aggregate->plainFullName();
577 json["typeHref"_L1] = QString(fileBase(aggregate) + "."_L1 + m_fileExtension);
578
579 QJsonArray summaryArr;
580 for (const Section *section : obsoleteSummary) {
581 QJsonObject sectionJson;
582 sectionJson["title"_L1] = section->title();
583 sectionJson["id"_L1] = section->title().toLower().replace(' '_L1, '-'_L1);
584 QJsonArray membersJson;
585 for (const Node *node : section->obsoleteMembers()) {
586 IR::MemberIR mir = NodeExtractor::extractMemberIR(
587 node, m_hrefResolver.get(), aggregate,
588 MemberExtractionLevel::Summary);
589 membersJson.append(mir.toJson());
590 }
591 sectionJson["members"_L1] = membersJson;
592 summaryArr.append(sectionJson);
593 }
594
595 QJsonArray detailArr;
596 for (const Section *section : obsoleteDetail) {
597 QJsonObject sectionJson;
598 sectionJson["title"_L1] = section->title();
599 QJsonArray membersJson;
600 for (const Node *node : section->obsoleteMembers()) {
601 IR::MemberIR mir = NodeExtractor::extractMemberIR(
602 node, m_hrefResolver.get(), aggregate,
603 MemberExtractionLevel::Detail);
604 membersJson.append(mir.toJson());
605 }
606 sectionJson["members"_L1] = membersJson;
607 detailArr.append(sectionJson);
608 }
609
610 json["sections"_L1] = summaryArr;
611 json["detailSections"_L1] = detailArr;
612
613 beginDocument(obsoleteFileName);
614 renderJson(json, "obsolete"_L1);
616}
617
618/*!
619 \internal
620 Resolves an Inja \c{{% include %}} directive to template content.
621
622 Searches the filesystem first (for user-customized templates) and then
623 Qt's embedded resource system (for bundled defaults). This enables
624 Inja's include mechanism to work with Qt resources, where
625 \c{std::ifstream} can't open \c{:/} paths.
626
627 Returns the file content, including an empty file, or no value if the
628 include isn't found in either location.
629*/
630std::optional<QString> TemplateGenerator::resolveInclude(const QString &name) const
631{
632 const auto loaded = loadTemplate(m_templateDir, name);
633 if (!loaded)
634 return std::nullopt;
635 return loaded->content;
636}
637
638/*!
639 \internal
640
641 Expansion runs first: the expander materializes catalog subtrees
642 from ListPlaceholder blocks, with entry hrefs already resolved
643 through HrefResolver at extraction time. The link resolver then
644 walks the fully-populated tree on its single pass, picking up
645 any inline links inside brief content the expander emitted.
646 Each pass guards itself, so callers don't need to know which
647 collaborators are present — and so every block-bearing field
648 gets both passes consistently rather than drifting whenever a
649 new field is added.
650
651 The resolver receives two roles: \a page, the output page node,
652 stays the URL and catalog context (hrefs must remain relative to
653 the rendered page); \a semantic is the node whose documentation
654 the content was authored in. Page-level fields keep \a page for
655 both, while detail member content resolves against the member
656 that carries it.
657*/
658static void processDocumentBlocks(IR::ListExpander *expander, LinkResolver *resolver,
659 IR::Document &ir, const Node *page,
660 const NodeExtractor::SemanticOrigins *origins)
661{
662 auto process = [&](QList<IR::ContentBlock> &blocks, const Node *semantic) {
663 if (blocks.isEmpty())
664 return;
665 if (expander)
666 expander->expand(blocks, page);
667 if (resolver)
668 resolver->resolve(blocks, { semantic, page });
669 };
670
671 process(ir.body, page);
672 if (ir.cppReferenceInfo)
673 process(ir.cppReferenceInfo->threadSafetyAdmonition, page);
674 // The origins list, when present, is paired with the detail sections
675 // and members built alongside it; the IR keeps that order.
676 Q_ASSERT(!origins || origins->size() == ir.detailSections.size());
677 for (int s = 0; s < ir.detailSections.size(); ++s) {
678 auto &section = ir.detailSections[s];
679 Q_ASSERT(!origins || origins->at(s).size() == section.members.size());
680 for (int m = 0; m < section.members.size(); ++m) {
681 auto &member = section.members[m];
682 // A detail member's body and see-also content is authored on a
683 // node other than the output page: the member itself, or the
684 // SharedCommentNode for copied shared text. The paired origin
685 // keeps that authorship context; a member with a nullptr
686 // origin (no anchor) falls back to the output page in
687 // LinkResolver::resolve().
688 const Node *semantic = page;
689 if (origins
690 && s < origins->size()
691 && m < origins->at(s).size())
692 semantic = origins->at(s).at(m);
693 process(member.body, semantic);
694 process(member.alsoList, semantic);
695 }
696 }
697}
698
699/*!
700 \internal
701 Computes the base filename for a node, delegating the core computation
702 to Utilities::computeFileBase().
703
704 This handles caching and adapts the TemplateGenerator's OutputContext-based
705 prefix/suffix lookup to the shared interface.
706
707 Like the legacy generators, the output name of a deprecated
708 non-aggregate node (a member or a page) carries an "-obsolete"
709 suffix, so links computed through HrefResolver resolve to the file
710 this generator writes. Aggregates keep their own page name.
711*/
712QString TemplateGenerator::fileBase(const Node *node) const
713{
714 const bool obsoleteFileBase = !node->isAggregate() && node->isDeprecated();
715
716 if (!node->isPageNode() && !node->isCollectionNode())
717 node = node->parent();
718
719 QString result;
720 if (node->hasFileNameBase()) {
721 result = node->fileNameBase();
722 } else {
723 result = Utilities::computeFileBase(
724 node, m_context->project,
725 [this](const Node *n) -> QString {
726 if (n->isCollectionNode())
727 return {};
728 return m_context->outputPrefix(n->genus());
729 },
730 [this](const Node *n) { return m_context->outputSuffix(n->genus()); });
731 const_cast<Node *>(node)->setFileNameBase(result);
732 }
733
734 if (obsoleteFileBase)
735 result += "-obsolete"_L1;
736 return result;
737}
738
739/*!
740 \internal
741 Creates the production FileDocumentWriter.
742
743 This is called during initialization to create the default writer
744 that writes to the filesystem. For testing, a mock writer can be
745 injected by setting m_writer before calling prepare().
746
747 Also initializes m_context with configuration values, enabling
748 OutputContext-based access to output settings.
749*/
750void TemplateGenerator::createDefaultWriter()
751{
752 // Initialize OutputContext from configuration
753 const Config &config = Config::instance();
754 m_context.emplace(OutputContext::fromConfig(config, format()));
755
756 if (m_writer)
757 return; // Writer already set (e.g., injected for testing)
758
759 m_writer = std::make_unique<FileDocumentWriter>(*m_context);
760}
761
762/*!
763 \internal
764 Walks content blocks recursively, resolving image filenames and copying
765 image files to the output directory.
766
767 For each InlineContent with type Image, the method resolves the raw
768 filename through FileResolver, copies the file to output/images/, and
769 prefixes the href with the images subdirectory path so that templates
770 render correct src attributes.
771
772*/
773void TemplateGenerator::resolveImagePaths(IR::Document &ir)
774{
775 if (!m_context)
776 return;
777
778 const QString &outDir = m_context->outputDir.path();
779 if (outDir.isEmpty())
780 return;
781
782 const QString imagesDir = u"images"_s;
783 const QString imagesDestDir = outDir + '/'_L1 + imagesDir;
784
785 QDir().mkpath(imagesDestDir);
786
787 auto resolveInlines = [&](QList<IR::InlineContent> &inlines) {
788 for (auto &inline_ : inlines) {
789 if (inline_.type != IR::InlineType::Image)
790 continue;
791
792 auto resolved = m_fileResolver.resolve(inline_.href);
793 if (!resolved)
794 continue;
795
796 const QString fileName = QFileInfo(resolved->get_path()).fileName();
797 QFile::copy(resolved->get_path(), imagesDestDir + '/'_L1 + fileName);
798 inline_.href = imagesDir + '/'_L1 + fileName;
799 }
800 };
801
802 std::function<void(QList<IR::ContentBlock> &)> walkBlocks;
803 walkBlocks = [&](QList<IR::ContentBlock> &blocks) {
804 for (auto &block : blocks) {
805 resolveInlines(block.inlineContent);
806 if (!block.children.isEmpty())
807 walkBlocks(block.children);
808 }
809 };
810
811 walkBlocks(ir.body);
812
813 for (auto &section : ir.detailSections) {
814 for (auto &member : section.members) {
815 walkBlocks(member.body);
816 walkBlocks(member.alsoList);
817 }
818 }
819}
820
821/*!
822 \internal
823 Copies static assets to the output directory using a two-tier resolution
824 strategy: templatedir assets take priority, with QRC defaults as fallback.
825
826 When a template directory provides an \c{assets/} subdirectory, all files
827 within it are copied recursively to the output directory, preserving the
828 subdirectory structure. This supports fonts, images, and other static
829 resources organized in subdirectories.
830
831 After copying theme assets, the method checks whether a stylesheet is
832 needed (controlled by \c{m_emitStylesheet}). If the theme didn't provide
833 one, the default QRC stylesheet is copied with the configured filename.
834*/
835void TemplateGenerator::copyAssets()
836{
837 if (!m_context)
838 return;
839
840 const QString &outDir = m_context->outputDir.path();
841 if (outDir.isEmpty())
842 return;
843
844 QSet<QString> themeAssets;
845
846 if (!m_templateDir.isEmpty()) {
847 QDir assetsDir(m_templateDir + "/assets"_L1);
848 if (assetsDir.exists()) {
849 QDirIterator it(assetsDir.path(), QDir::Files,
850 QDirIterator::Subdirectories);
851 int count = 0;
852 while (it.hasNext()) {
853 it.next();
854 QString rel = assetsDir.relativeFilePath(it.filePath());
855 QString dst = outDir + '/'_L1 + rel;
856 QDir().mkpath(QFileInfo(dst).path());
857 QFile::remove(dst);
858 if (QFile::copy(it.filePath(), dst)) {
859 themeAssets.insert(rel);
860 qCDebug(lcQDocTemplateGenerator) << "[%1]"_L1.arg(m_format) << "Asset (theme):" << rel;
861 ++count;
862 } else {
863 qCWarning(lcQDocTemplateGenerator)
864 << "[%1]"_L1.arg(m_format) << "Failed to copy asset:" << it.filePath() << "->" << dst;
865 }
866 }
867 if (count > 0)
868 qCDebug(lcQDocTemplateGenerator) << "[%1]"_L1.arg(m_format) << "Copied" << count << "theme asset(s)";
869 }
870 }
871
872 if (m_emitStylesheet && !themeAssets.contains(m_stylesheetName)) {
873 const QString dstCss = outDir + '/'_L1 + m_stylesheetName;
874 QFile::remove(dstCss);
875 QFile::copy(":/qdoc/templates/assets/qdoc-default.css"_L1, dstCss);
876 QFile(dstCss).setPermissions(QFile::ReadOwner | QFile::WriteOwner
877 | QFile::ReadGroup | QFile::ReadOther);
878 qCDebug(lcQDocTemplateGenerator) << "[%1]"_L1.arg(m_format) << "Asset (QRC fallback):" << m_stylesheetName;
879 }
880}
881
882QT_END_NAMESPACE
A class for holding the members of a collection of doc pages.
contains all the information for a single config variable in a .qdocconf file.
Definition config.h:43
The Config class contains the configuration variables for controlling how qdoc produces documentation...
Definition config.h:95
InclusionPolicy createInclusionPolicy() const
Definition config.cpp:1621
Traverses the node tree and dispatches to a handler for documentation generation.
void traverse(Node *root, DocumentationHandler &handler)
Encapsulate the logic that QDoc uses to find files whose path is provided by the user and that are re...
static bool noLinkErrors()
Definition generator.h:86
static QmlTypeNode * qmlTypeContext()
Definition generator.h:92
static bool autolinkErrors()
Definition generator.h:87
Assembles IR Documents from pre-extracted metadata.
Definition builder.h:15
Document buildPageIR(PageMetadata pm) const
Definition builder.cpp:196
Singleton registry for discovering output producers by format.
void registerProducer(OutputProducer *producer)
Registers producer with this registry.
void unregisterProducer(OutputProducer *producer)
Unregisters producer from this registry.
static OutputProducerRegistry & instance()
Returns the singleton registry instance.
A PageNode is a Node that generates a documentation page.
Definition pagenode.h:19
This class provides exclusive access to the qdoc database, which consists of a forrest of trees and a...
NamespaceNode * primaryTreeRoot()
Returns a pointer to the root node of the primary tree.
void mergeCollections(CollectionNode *c)
Finds all the collection nodes with the same name and type as c and merges their members into the mem...
A class for creating vectors of collections for documentation.
Definition sections.h:80
Sections(const Aggregate *aggregate)
This constructor builds the section vectors based on the type of the aggregate node.
Definition sections.cpp:372
bool hasObsoleteMembers(SectionPtrVector *summary_spv, SectionPtrVector *details_spv) const
Returns true if any sections in this object contain obsolete members.
Definition sections.cpp:967
Generates documentation using external templates and a pre-built IR.
void generateProxyPage(Aggregate *aggregate, CodeMarker *marker) override
void endDocument() override
void beginDocument(const QString &fileName) override
void generateCppReferencePage(Aggregate *aggregate, CodeMarker *marker) override
std::function< void(const QString &message)> RenderFailureHandler
void generatePageNode(PageNode *pn, CodeMarker *marker) override
void finalize() override
Finalizes output production.
void prepare() override
Prepares the producer for an output run.
QString fileExtension() const
QString fileName(const Node *node) const override
void produce() override
Produces documentation output.
void generateCollectionNode(CollectionNode *cn, CodeMarker *marker) override
QString format() const override
Returns the format identifier for this producer (e.g., "HTML", "DocBook", "template").
void mergeCollections(CollectionNode *cn) override
void generateQmlTypePage(QmlTypeNode *qcn, CodeMarker *marker) override
TemplateGenerator(FileResolver &fileResolver, QDocDatabase &qdb, const QString &format, RenderFailureHandler reportFailure)
Constructs a producer for format using fileResolver and qdb.
void generateGenericCollectionPage(CollectionNode *cn, CodeMarker *marker) override
Definition builder.cpp:14
std::optional< IR::AllMembersIR > extractAllMembersIR(const PageNode *pn, const HrefResolver *hrefResolver)
Combined button and popup list for selecting options.
QList< const Section * > SectionPtrVector
Definition sections.h:77
Intermediate representation of the all-members listing page.
Definition member.h:119
Intermediate representation for a documentation topic.
Definition document.h:201
The Node class is the base class for all the nodes in QDoc's parse tree.
bool hasFileNameBase() const
Returns true if the node's file name base has been set.
Definition node.h:169
virtual bool isPageNode() const
Returns true if this node represents something that generates a documentation page.
Definition node.h:150
virtual bool isTextPageNode() const
Returns true if the node is a PageNode but not an Aggregate.
Definition node.h:155
Aggregate * parent() const
Returns the node's parent pointer, that is, the aggregate under which the node is documented.
Definition node.h:211
virtual bool isDeprecated() const
Returns true if this node's status is Deprecated.
Definition node.h:136
virtual bool isAggregate() const
Returns true if this node is an aggregate, which means it inherits Aggregate and can therefore have c...
Definition node.h:138
virtual bool isCollectionNode() const
Returns true if this is an instance of CollectionNode.
Definition node.h:146
static void processDocumentBlocks(IR::ListExpander *expander, LinkResolver *resolver, IR::Document &ir, const Node *page, const NodeExtractor::SemanticOrigins *origins=nullptr)
static void writeOrDiscardRenderResult(FileDocumentWriter *writer, const InjaBridge::RenderContext &context, const InjaBridge::RenderResult &result, const TemplateGenerator::RenderFailureHandler &reportFailure)
static std::optional< LoadedTemplate > loadTemplate(const QString &templateDir, const QString &name)