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"
7#include "codemarker.h"
9#include "config.h"
12#include "documentwriter.h"
13#include "generator.h"
14#include "hrefresolver.h"
15#include "injabridge.h"
16#include "ir/builder.h"
17#include "ir/document.h"
18#include "linkresolver.h"
19#include "node.h"
20#include "nodeextractor.h"
21#include "outputcontext.h"
23#include "pagenode.h"
24#include "qdocdatabase.h"
25#include "qmltypenode.h"
26#include "sections.h"
27#include "tree.h"
28#include "utilities.h"
29
30#include <utility>
31
32#include <QtCore/qdir.h>
33#include <QtCore/qdiriterator.h>
34#include <QtCore/qfile.h>
35#include <QtCore/qfileinfo.h>
36#include <QtCore/qloggingcategory.h>
37#include <QtCore/qtextstream.h>
38
40
41Q_LOGGING_CATEGORY(lcQDocTemplateGenerator, "qt.qdoc.templategenerator")
42
43using namespace Qt::Literals;
44
45static QString nodeTypeKey(const Node *node);
46static void resolveDocumentLinks(LinkResolver *resolver, IR::Document &ir, const Node *relative);
47
48/*!
49 \class TemplateGenerator
50 \internal
51 \brief Generates documentation using external templates and a pre-built IR.
52
53 TemplateGenerator implements OutputProducer and DocumentationHandler to
54 generate documentation without inheriting from Generator. It uses
55 DocumentationTraverser for tree traversal and delegates content generation
56 to templates via the IR system.
57
58 \section1 Architecture
59
60 The generator follows a composition-based design:
61 \list
62 \li \b{OutputProducer}: Lifecycle interface (prepare/produce/finalize).
63 \li \b{DocumentationHandler}: Content generation callbacks for
64 traverser.
65 \li \b{DocumentationTraverser}: Shared tree traversal logic.
66 \li \b{DocumentWriter}: Output abstraction (file or string for tests).
67 \endlist
68
69 \sa DocumentationTraverser, DocumentationHandler, OutputProducer,
70 IR::Builder
71*/
72
74 const QString &format)
75 : m_fileResolver(fileResolver)
76 , m_qdb(qdb)
77 , m_format(format.isEmpty() ? u"template"_s : format)
78{
80}
81
86
88{
89 createDefaultWriter();
90
91 const Config &config = Config::instance();
92
93 QString extensionConfig = config.get(m_format + ".extension"_L1).asString();
94 if (!extensionConfig.isEmpty())
95 m_fileExtension = extensionConfig;
96
97 QString templateDirConfig = config.get(m_format + ".templatedir"_L1).asString();
98
99 if (templateDirConfig.isEmpty()) {
100 m_templateDir.clear();
101 } else if (QDir::isAbsolutePath(templateDirConfig)) {
102 m_templateDir = templateDirConfig;
103 } else {
104 // Relative path: resolve relative to the qdocconf file's directory,
105 // consistent with how sourcedirs, headerdirs, etc. are resolved.
106 m_templateDir = QDir::cleanPath(
107 QDir(config.currentDir()).absoluteFilePath(templateDirConfig));
108 }
109
110 bool foundTemplates = false;
111 if (!m_templateDir.isEmpty()) {
112 QDir templateDir(m_templateDir);
113 if (templateDir.exists() && !templateDir.entryList(QDir::Files).isEmpty()) {
114 foundTemplates = true;
115 qCInfo(lcQDocTemplateGenerator) << "[%1]"_L1.arg(m_format) << "Using template directory:" << m_templateDir;
116 } else if (!templateDir.exists()) {
117 qCInfo(lcQDocTemplateGenerator)
118 << "[%1]"_L1.arg(m_format) << "Configured template directory does not exist:" << m_templateDir
119 << "- will use embedded templates";
120 } else {
121 qCInfo(lcQDocTemplateGenerator)
122 << "[%1]"_L1.arg(m_format) << "Configured template directory is empty:" << m_templateDir
123 << "- will use embedded templates";
124 }
125 } else {
126 qCInfo(lcQDocTemplateGenerator)
127 << "[%1]"_L1.arg(m_format) << "No external template directory configured - will use embedded templates";
128 }
129
130 if (!foundTemplates)
131 m_templateDir.clear();
132
133 m_emitStylesheet = config.get(m_format + ".stylesheet"_L1).asBool();
134
135 m_stylesheetName = config.get(m_format + ".stylesheetname"_L1).asString();
136 if (m_stylesheetName.isEmpty())
137 m_stylesheetName = u"qdoc-default.css"_s;
138
139 // Enforce plain filename — no directory separators, no traversal.
140 // The stylesheet is copied to and referenced from the output root,
141 // so subpaths would create inconsistencies between theme-provided
142 // assets and the QRC fallback.
143 if (m_stylesheetName.contains('/'_L1)
144 || m_stylesheetName.contains('\\'_L1)
145 || m_stylesheetName.contains(".."_L1)
146 || QDir::isAbsolutePath(m_stylesheetName)) {
147 qCWarning(lcQDocTemplateGenerator)
148 << "[%1]"_L1.arg(m_format) << "Ignoring stylesheetname:" << m_stylesheetName
149 << "— must be a plain filename (no path separators)";
150 m_stylesheetName = u"qdoc-default.css"_s;
151 }
152
153 copyAssets();
154
155 // Construct link resolvers using OutputContext data.
156 // TemplateGenerator doesn't inherit from Generator, so we use
157 // m_context (OutputContext) which has the same prefix/suffix data.
158 HrefResolverConfig hrefConfig;
159 hrefConfig.project = m_context->project;
160 hrefConfig.fileExtension = m_fileExtension;
161 hrefConfig.useOutputSubdirs = m_context->useSubdirs;
162 hrefConfig.noLinkErrors = Generator::noLinkErrors();
163 hrefConfig.inclusionPolicy = config.createInclusionPolicy();
164 hrefConfig.outputPrefixFn = [this](const Node *node) {
165 return m_context->outputPrefix(nodeTypeKey(node));
166 };
167 hrefConfig.outputSuffixFn = [this](const Node *node) {
168 return m_context->outputSuffix(nodeTypeKey(node));
169 };
170 hrefConfig.cleanRefFn = [](const QString &ref) { return Generator::cleanRef(ref); };
171 hrefConfig.qmlTypeContextFn = []() -> const QmlTypeNode * {
173 };
174 m_hrefResolver = std::make_unique<HrefResolver>(hrefConfig);
175
176 LinkResolverConfig linkConfig;
177 linkConfig.autolinkErrors = Generator::autolinkErrors();
178 linkConfig.noLinkErrors = Generator::noLinkErrors();
179 m_linkResolver = std::make_unique<LinkResolver>(&m_qdb, *m_hrefResolver, linkConfig);
180}
181
183{
184 DocumentationTraverser traverser;
185 Node *root = m_qdb.primaryTreeRoot();
186 if (root)
187 traverser.traverse(root, *this);
188}
189
191{
192 m_writer.reset();
193}
194
196{
197 return m_format;
198}
199
200void TemplateGenerator::beginDocument(const QString &outputFileName)
201{
202 if (m_writer)
203 m_writer->beginDocument(outputFileName);
204}
205
207{
208 if (m_writer)
209 m_writer->endDocument();
210}
211
213{
214 if (!node->url().isEmpty())
215 return node->url();
216
217 // Special case for simple page nodes (\page commands) with explicit
218 // non-.html extensions. Use the normalized fileBase() but preserve
219 // user specified extension
221 QFileInfo originalName(node->name());
222 QString suffix = originalName.suffix();
223 if (!suffix.isEmpty() && suffix != "html"_L1) {
224 // User specified a non-.html extension - use normalized base + original extension
225 QString name = fileBase(node);
226 return name + '.'_L1 + suffix;
227 }
228 }
229
230 QString name = fileBase(node) + '.'_L1;
231 return name + m_fileExtension;
232}
233
235{
236 Q_UNUSED(marker);
237
238 IR::PageMetadata pm = NodeExtractor::extractPageMetadata(cn, m_hrefResolver.get());
239
240 IR::Builder builder;
241 IR::Document ir = builder.buildPageIR(std::move(pm));
242
243 if (m_linkResolver)
244 resolveDocumentLinks(m_linkResolver.get(), ir, cn);
245
246 resolveImagePaths(ir);
247 renderDocument(ir, "collection"_L1);
248}
249
251{
252 Q_UNUSED(marker);
253
254 IR::PageMetadata pm = NodeExtractor::extractPageMetadata(cn, m_hrefResolver.get());
255
256 IR::Builder builder;
257 IR::Document ir = builder.buildPageIR(std::move(pm));
258
259 if (m_linkResolver)
260 resolveDocumentLinks(m_linkResolver.get(), ir, cn);
261
262 resolveImagePaths(ir);
263 renderDocument(ir, "collection"_L1);
264}
265
267{
268 Q_UNUSED(marker);
269
270 IR::PageMetadata pm = NodeExtractor::extractPageMetadata(pn, m_hrefResolver.get());
271
272 IR::Builder builder;
273 IR::Document ir = builder.buildPageIR(std::move(pm));
274
275 if (m_linkResolver)
276 resolveDocumentLinks(m_linkResolver.get(), ir, pn);
277
278 resolveImagePaths(ir);
279 renderDocument(ir, "page"_L1);
280}
281
283{
284 Q_UNUSED(marker);
285
286 IR::PageMetadata pm = NodeExtractor::extractPageMetadata(aggregate, m_hrefResolver.get());
287 auto allMembers = NodeExtractor::extractAllMembersIR(aggregate, m_hrefResolver.get());
288
289 IR::Builder builder;
290 IR::Document ir = builder.buildPageIR(std::move(pm));
291
292 if (allMembers)
293 ir.membersPageUrl = fileBase(aggregate) + "-members."_L1 + m_fileExtension;
294
295 if (ir.cppReferenceInfo && ir.cppReferenceInfo->hasObsoleteMembers)
296 ir.cppReferenceInfo->obsoleteMembersUrl =
297 fileBase(aggregate) + "-obsolete."_L1 + m_fileExtension;
298
299 if (m_linkResolver)
300 resolveDocumentLinks(m_linkResolver.get(), ir, aggregate);
301
302 resolveImagePaths(ir);
303 renderDocument(ir, "cppref"_L1);
304
305 if (allMembers)
306 generateMemberListingPage(aggregate, *allMembers);
307
308 if (ir.cppReferenceInfo && ir.cppReferenceInfo->hasObsoleteMembers)
309 generateObsoleteMembersPage(aggregate);
310}
311
313{
314 Q_UNUSED(marker);
315
316 IR::PageMetadata pm = NodeExtractor::extractPageMetadata(qcn, m_hrefResolver.get());
317 auto allMembers = NodeExtractor::extractAllMembersIR(qcn, m_hrefResolver.get());
318
319 IR::Builder builder;
320 IR::Document ir = builder.buildPageIR(std::move(pm));
321
322 if (allMembers)
323 ir.membersPageUrl = fileBase(qcn) + "-members."_L1 + m_fileExtension;
324
325 if (m_linkResolver)
326 resolveDocumentLinks(m_linkResolver.get(), ir, qcn);
327
328 resolveImagePaths(ir);
329 renderDocument(ir, "qmltype"_L1);
330
331 if (allMembers)
332 generateMemberListingPage(qcn, *allMembers);
333}
334
336{
337 Q_UNUSED(marker);
338
339 // Placeholder - IR integration pending
340 if (m_writer && m_writer->isOpen()) {
341 m_writer->writeLine(QString(u"<!-- TemplateGenerator: Proxy Page for "_s
342 + aggregate->name() + u" -->"_s));
343 m_writer->writeLine(QString(u"<h1>"_s + aggregate->fullTitle() + u"</h1>"_s));
344 m_writer->writeLine(u"<p>Template-based output (IR integration pending)</p>"_s);
345 }
346}
347
352
354{
355 return m_fileExtension;
356}
357
358/*!
359 \internal
360 Render phase: Format pre-built IR according to a template.
361
362 This is TemplateGenerator's core responsibility. It receives IR and
363 produces formatted output without any knowledge of Nodes or the database.
364*/
365void TemplateGenerator::renderDocument(const IR::Document &ir, const QString &templateBaseName)
366{
367 const QString templateFileName = templateBaseName + '.'_L1 + m_fileExtension;
368 QString templateContent;
369
370 if (!m_templateDir.isEmpty()) {
371 QString templatePath = m_templateDir + '/'_L1 + templateFileName;
372 QFile templateFile(templatePath);
373
374 if (templateFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
375 templateContent = QString::fromUtf8(templateFile.readAll());
376 templateFile.close();
377 }
378 }
379
380 if (templateContent.isEmpty()) {
381 QFile resourceFile(":/qdoc/templates/"_L1 + templateFileName);
382 if (resourceFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
383 templateContent = QString::fromUtf8(resourceFile.readAll());
384 resourceFile.close();
385 }
386 }
387
388 if (templateContent.isEmpty())
389 qFatal("TemplateGenerator[%s]: No template file found for extension '%s'. "
390 "Ensure '%s.%s' exists in the configured template directory or in resources.",
391 qPrintable(m_format), qPrintable(m_fileExtension),
392 qPrintable(templateBaseName), qPrintable(m_fileExtension));
393
394 QJsonObject json = ir.toJson();
395 json["stylesheetEnabled"_L1] = m_emitStylesheet;
396 json["stylesheetName"_L1] = m_stylesheetName;
397
398 auto includeCallback = [this](const QString &name) { return resolveInclude(name); };
399 QString rendered = InjaBridge::render(templateContent, json, includeCallback);
400
401 if (m_writer && m_writer->isOpen())
402 m_writer->write(rendered);
403}
404
405/*!
406 \internal
407 Render a raw QJsonObject through a named template.
408
409 Unlike renderDocument(), this takes an arbitrary JSON object rather
410 than an IR::Document. It's used for sub-pages (such as the member
411 listing page) where the data structure differs from Document's.
412*/
413void TemplateGenerator::renderJson(const QJsonObject &json, const QString &templateBaseName)
414{
415 const QString templateFileName = templateBaseName + '.'_L1 + m_fileExtension;
416 QString templateContent;
417
418 if (!m_templateDir.isEmpty()) {
419 QString templatePath = m_templateDir + '/'_L1 + templateFileName;
420 QFile templateFile(templatePath);
421
422 if (templateFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
423 templateContent = QString::fromUtf8(templateFile.readAll());
424 templateFile.close();
425 }
426 }
427
428 if (templateContent.isEmpty()) {
429 QFile resourceFile(":/qdoc/templates/"_L1 + templateFileName);
430 if (resourceFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
431 templateContent = QString::fromUtf8(resourceFile.readAll());
432 resourceFile.close();
433 }
434 }
435
436 if (templateContent.isEmpty())
437 qFatal("TemplateGenerator[%s]: No template file found for '%s'. "
438 "Ensure '%s.%s' exists in the configured template directory or in resources.",
439 qPrintable(m_format), qPrintable(templateBaseName),
440 qPrintable(templateBaseName), qPrintable(m_fileExtension));
441
442 QJsonObject enrichedJson = json;
443 enrichedJson["stylesheetEnabled"_L1] = m_emitStylesheet;
444 enrichedJson["stylesheetName"_L1] = m_stylesheetName;
445
446 auto includeCallback = [this](const QString &name) { return resolveInclude(name); };
447 QString rendered = InjaBridge::render(templateContent, enrichedJson, includeCallback);
448
449 if (m_writer && m_writer->isOpen())
450 m_writer->write(rendered);
451}
452
453/*!
454 \internal
455 Generate a member listing sub-page for a C++ class or QML type.
456
457 Opens a new output file, renders the all-members data through the
458 \c members template, and closes the file. This mirrors
459 HtmlGenerator::generateAllMembersFile() but uses the IR pipeline
460 and template system instead of inline HTML generation.
461*/
462void TemplateGenerator::generateMemberListingPage(const Node *node,
463 const IR::AllMembersIR &allMembers)
464{
465 const QString membersFileName =
466 fileBase(node) + "-members."_L1 + m_fileExtension;
467
468 QJsonObject json = allMembers.toJson();
469 json["title"_L1] = QString("List of All Members for "_L1 + allMembers.typeName);
470
471 beginDocument(membersFileName);
472 renderJson(json, "members"_L1);
474}
475
476/*!
477 \internal
478 Generate a sub-page listing obsolete members for a C++ aggregate.
479
480 Opens a new output file, extracts obsolete summary and detail
481 sections through the Sections API, converts each member to MemberIR
482 JSON, and renders the result through the \c obsolete template.
483*/
484void TemplateGenerator::generateObsoleteMembersPage(const Aggregate *aggregate)
485{
486 Sections sections(aggregate);
487 SectionPtrVector obsoleteSummary;
488 SectionPtrVector obsoleteDetail;
489
490 if (!sections.hasObsoleteMembers(&obsoleteSummary, &obsoleteDetail))
491 return;
492
493 const QString obsoleteFileName =
494 fileBase(aggregate) + "-obsolete."_L1 + m_fileExtension;
495
496 QJsonObject json;
497 json["title"_L1] = QString("Obsolete Members for "_L1 + aggregate->plainFullName());
498 json["typeName"_L1] = aggregate->plainFullName();
499 json["typeHref"_L1] = QString(fileBase(aggregate) + "."_L1 + m_fileExtension);
500
501 QJsonArray summaryArr;
502 for (const Section *section : obsoleteSummary) {
503 QJsonObject sectionJson;
504 sectionJson["title"_L1] = section->title();
505 sectionJson["id"_L1] = section->title().toLower().replace(' '_L1, '-'_L1);
506 QJsonArray membersJson;
507 for (const Node *node : section->obsoleteMembers()) {
508 IR::MemberIR mir = NodeExtractor::extractMemberIR(
509 node, m_hrefResolver.get(), aggregate,
510 MemberExtractionLevel::Summary);
511 membersJson.append(mir.toJson());
512 }
513 sectionJson["members"_L1] = membersJson;
514 summaryArr.append(sectionJson);
515 }
516
517 QJsonArray detailArr;
518 for (const Section *section : obsoleteDetail) {
519 QJsonObject sectionJson;
520 sectionJson["title"_L1] = section->title();
521 QJsonArray membersJson;
522 for (const Node *node : section->obsoleteMembers()) {
523 IR::MemberIR mir = NodeExtractor::extractMemberIR(
524 node, m_hrefResolver.get(), aggregate,
525 MemberExtractionLevel::Detail);
526 membersJson.append(mir.toJson());
527 }
528 sectionJson["members"_L1] = membersJson;
529 detailArr.append(sectionJson);
530 }
531
532 json["sections"_L1] = summaryArr;
533 json["detailSections"_L1] = detailArr;
534
535 beginDocument(obsoleteFileName);
536 renderJson(json, "obsolete"_L1);
538}
539
540/*!
541 \internal
542 Resolves an Inja \c{{% include %}} directive to template content.
543
544 Searches the filesystem first (for user-customized templates) and then
545 Qt's embedded resource system (for bundled defaults). This enables
546 Inja's include mechanism to work with Qt resources, where
547 \c{std::ifstream} can't open \c{:/} paths.
548
549 Returns the file content as a QString, or an empty QString if the file
550 isn't found in either location.
551*/
552QString TemplateGenerator::resolveInclude(const QString &name) const
553{
554 if (!m_templateDir.isEmpty()) {
555 QFile file(m_templateDir + '/'_L1 + name);
556 if (file.open(QIODevice::ReadOnly | QIODevice::Text))
557 return QString::fromUtf8(file.readAll());
558 }
559
560 QFile resourceFile(":/qdoc/templates/"_L1 + name);
561 if (resourceFile.open(QIODevice::ReadOnly | QIODevice::Text))
562 return QString::fromUtf8(resourceFile.readAll());
563
564 return {};
565}
566
567/*!
568 \internal
569 Returns the output prefix/suffix key for a node based on its genus.
570
571 This is used to look up configured prefixes and suffixes from OutputContext.
572*/
573static QString nodeTypeKey(const Node *node)
574{
575 if (node->isPageNode()) {
576 switch (node->genus()) {
577 case Genus::QML:
578 return u"QML"_s;
579 case Genus::CPP:
580 return u"CPP"_s;
581 default:
582 break;
583 }
584 }
585 return QString();
586}
587
588static void resolveDocumentLinks(LinkResolver *resolver, IR::Document &ir, const Node *relative)
589{
590 if (!ir.body.isEmpty())
591 resolver->resolve(ir.body, relative);
592 if (ir.cppReferenceInfo && !ir.cppReferenceInfo->threadSafetyAdmonition.isEmpty())
593 resolver->resolve(ir.cppReferenceInfo->threadSafetyAdmonition, relative);
594 for (auto &section : ir.detailSections) {
595 for (auto &member : section.members) {
596 if (!member.body.isEmpty())
597 resolver->resolve(member.body, relative);
598 if (!member.alsoList.isEmpty())
599 resolver->resolve(member.alsoList, relative);
600 }
601 }
602}
603
604/*!
605 \internal
606 Computes the base filename for a node, delegating the core computation
607 to Utilities::computeFileBase().
608
609 This handles caching and adapts the TemplateGenerator's OutputContext-based
610 prefix/suffix lookup to the shared interface.
611*/
612QString TemplateGenerator::fileBase(const Node *node) const
613{
614 if (!node->isPageNode() && !node->isCollectionNode())
615 node = node->parent();
616
617 if (node->hasFileNameBase())
618 return node->fileNameBase();
619
620 QString result = Utilities::computeFileBase(
621 node, m_context->project,
622 [this](const Node *n) -> QString {
623 if (n->isCollectionNode())
624 return {};
625 return m_context->outputPrefix(nodeTypeKey(n));
626 },
627 [this](const Node *n) { return m_context->outputSuffix(nodeTypeKey(n)); });
628
629 const_cast<Node *>(node)->setFileNameBase(result);
630 return result;
631}
632
633/*!
634 \internal
635 Creates the production FileDocumentWriter.
636
637 This is called during initialization to create the default writer
638 that writes to the filesystem. For testing, a mock writer can be
639 injected by setting m_writer before calling prepare().
640
641 Also initializes m_context with configuration values, enabling
642 OutputContext-based access to output settings.
643*/
644void TemplateGenerator::createDefaultWriter()
645{
646 // Initialize OutputContext from configuration
647 const Config &config = Config::instance();
648 m_context.emplace(OutputContext::fromConfig(config, format()));
649
650 if (m_writer)
651 return; // Writer already set (e.g., injected for testing)
652
653 m_writer = std::make_unique<FileDocumentWriter>(*m_context);
654}
655
656/*!
657 \internal
658 Walks content blocks recursively, resolving image filenames and copying
659 image files to the output directory.
660
661 For each InlineContent with type Image, the method resolves the raw
662 filename through FileResolver, copies the file to output/images/, and
663 prefixes the href with the images subdirectory path so that templates
664 render correct src attributes.
665
666*/
667void TemplateGenerator::resolveImagePaths(IR::Document &ir)
668{
669 if (!m_context)
670 return;
671
672 const QString &outDir = m_context->outputDir.path();
673 if (outDir.isEmpty())
674 return;
675
676 const QString imagesDir = u"images"_s;
677 const QString imagesDestDir = outDir + '/'_L1 + imagesDir;
678
679 QDir().mkpath(imagesDestDir);
680
681 auto resolveInlines = [&](QList<IR::InlineContent> &inlines) {
682 for (auto &inline_ : inlines) {
683 if (inline_.type != IR::InlineType::Image)
684 continue;
685
686 auto resolved = m_fileResolver.resolve(inline_.href);
687 if (!resolved)
688 continue;
689
690 const QString fileName = QFileInfo(resolved->get_path()).fileName();
691 QFile::copy(resolved->get_path(), imagesDestDir + '/'_L1 + fileName);
692 inline_.href = imagesDir + '/'_L1 + fileName;
693 }
694 };
695
696 std::function<void(QList<IR::ContentBlock> &)> walkBlocks;
697 walkBlocks = [&](QList<IR::ContentBlock> &blocks) {
698 for (auto &block : blocks) {
699 resolveInlines(block.inlineContent);
700 if (!block.children.isEmpty())
701 walkBlocks(block.children);
702 }
703 };
704
705 walkBlocks(ir.body);
706
707 for (auto &section : ir.detailSections) {
708 for (auto &member : section.members) {
709 walkBlocks(member.body);
710 walkBlocks(member.alsoList);
711 }
712 }
713}
714
715/*!
716 \internal
717 Copies static assets to the output directory using a two-tier resolution
718 strategy: templatedir assets take priority, with QRC defaults as fallback.
719
720 When a template directory provides an \c{assets/} subdirectory, all files
721 within it are copied recursively to the output directory, preserving the
722 subdirectory structure. This supports fonts, images, and other static
723 resources organized in subdirectories.
724
725 After copying theme assets, the method checks whether a stylesheet is
726 needed (controlled by \c{m_emitStylesheet}). If the theme didn't provide
727 one, the default QRC stylesheet is copied with the configured filename.
728*/
729void TemplateGenerator::copyAssets()
730{
731 if (!m_context)
732 return;
733
734 const QString &outDir = m_context->outputDir.path();
735 if (outDir.isEmpty())
736 return;
737
738 QSet<QString> themeAssets;
739
740 if (!m_templateDir.isEmpty()) {
741 QDir assetsDir(m_templateDir + "/assets"_L1);
742 if (assetsDir.exists()) {
743 QDirIterator it(assetsDir.path(), QDir::Files,
744 QDirIterator::Subdirectories);
745 int count = 0;
746 while (it.hasNext()) {
747 it.next();
748 QString rel = assetsDir.relativeFilePath(it.filePath());
749 QString dst = outDir + '/'_L1 + rel;
750 QDir().mkpath(QFileInfo(dst).path());
751 QFile::remove(dst);
752 if (QFile::copy(it.filePath(), dst)) {
753 themeAssets.insert(rel);
754 qCDebug(lcQDocTemplateGenerator) << "[%1]"_L1.arg(m_format) << "Asset (theme):" << rel;
755 ++count;
756 } else {
757 qCWarning(lcQDocTemplateGenerator)
758 << "[%1]"_L1.arg(m_format) << "Failed to copy asset:" << it.filePath() << "->" << dst;
759 }
760 }
761 if (count > 0)
762 qCInfo(lcQDocTemplateGenerator) << "[%1]"_L1.arg(m_format) << "Copied" << count << "theme asset(s)";
763 }
764 }
765
766 if (m_emitStylesheet && !themeAssets.contains(m_stylesheetName)) {
767 const QString dstCss = outDir + '/'_L1 + m_stylesheetName;
768 QFile::remove(dstCss);
769 QFile::copy(":/qdoc/templates/assets/qdoc-default.css"_L1, dstCss);
770 QFile(dstCss).setPermissions(QFile::ReadOwner | QFile::WriteOwner
771 | QFile::ReadGroup | QFile::ReadOther);
772 qCDebug(lcQDocTemplateGenerator) << "[%1]"_L1.arg(m_format) << "Asset (QRC fallback):" << m_stylesheetName;
773 }
774}
775
776QT_END_NAMESPACE
A class for holding the members of a collection of doc pages.
The Config class contains the configuration variables for controlling how qdoc produces documentation...
Definition config.h:85
InclusionPolicy createInclusionPolicy() const
Definition config.cpp:1608
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:85
static QmlTypeNode * qmlTypeContext()
Definition generator.h:91
static bool autolinkErrors()
Definition generator.h:86
Assembles IR Documents from pre-extracted metadata.
Definition builder.h:15
Document buildPageIR(PageMetadata pm) const
Definition builder.cpp:156
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:371
bool hasObsoleteMembers(SectionPtrVector *summary_spv, SectionPtrVector *details_spv) const
Returns true if any sections in this object contain obsolete members.
Definition sections.cpp:963
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
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
TemplateGenerator(FileResolver &fileResolver, QDocDatabase &qdb, const QString &format=QString())
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
void generateGenericCollectionPage(CollectionNode *cn, CodeMarker *marker) override
Definition builder.cpp:14
std::optional< IR::AllMembersIR > extractAllMembersIR(const PageNode *pn, const HrefResolver *hrefResolver)
IR::PageMetadata extractPageMetadata(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:115
Intermediate representation for a documentation topic.
Definition document.h:162
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:167
Genus genus() const override
Returns this node's Genus.
Definition node.h:85
virtual bool isPageNode() const
Returns true if this node represents something that generates a documentation page.
Definition node.h:148
virtual bool isTextPageNode() const
Returns true if the node is a PageNode but not an Aggregate.
Definition node.h:153
Aggregate * parent() const
Returns the node's parent pointer.
Definition node.h:208
virtual bool isCollectionNode() const
Returns true if this is an instance of CollectionNode.
Definition node.h:144
static QString nodeTypeKey(const Node *node)
static void resolveDocumentLinks(LinkResolver *resolver, IR::Document &ir, const Node *relative)