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