17#include "ir/builder.h"
18#include "ir/document.h"
19#include "ir/listexpander.h"
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>
45Q_LOGGING_CATEGORY(lcQDocTemplateGenerator,
"qt.qdoc.templategenerator")
47using namespace Qt::Literals;
51 const NodeExtractor::SemanticOrigins *origins =
nullptr);
63 if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
65 return LoadedTemplate{ std::move(path), QString::fromUtf8(file.readAll()) };
68 if (!templateDir.isEmpty()) {
69 auto loaded = read(templateDir +
'/'_L1 + name);
74 return read(
":/qdoc/templates/"_L1 + name);
78 const InjaBridge::RenderContext &context,
79 const InjaBridge::RenderResult &result,
82 if (
const auto *failure = std::get_if<InjaBridge::RenderFailure>(&result)) {
83 reportFailure(InjaBridge::renderErrorText(context, failure->message));
85 writer->discardDocument();
86 }
else if (writer && writer->isOpen()) {
87 writer->write(std::get<QString>(result));
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
117
118
119
120
123 : m_fileResolver(fileResolver)
138 createDefaultWriter();
140 const Config &config = Config::instance();
142 QString extensionConfig = config.get(m_format +
".extension"_L1).asString();
143 if (!extensionConfig.isEmpty())
144 m_fileExtension = extensionConfig;
149 m_context->fileExtension = m_fileExtension;
151 const ConfigVar &templateDirVar = config.get(m_format +
".templatedir"_L1);
152 QString templateDirConfig = templateDirVar.asString();
154 if (templateDirConfig.isEmpty()) {
155 m_templateDir.clear();
156 }
else if (QDir::isAbsolutePath(templateDirConfig)) {
157 m_templateDir = templateDirConfig;
170 const QString base = templateDirVar.path().isEmpty()
171 ? config.currentDir()
172 : templateDirVar.path();
173 m_templateDir = QDir::cleanPath(QDir(base).absoluteFilePath(templateDirConfig));
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";
187 qCInfo(lcQDocTemplateGenerator)
188 <<
"[%1]"_L1.arg(m_format) <<
"Configured template directory is empty:" << m_templateDir
189 <<
"- will use embedded templates";
192 qCDebug(lcQDocTemplateGenerator)
193 <<
"[%1]"_L1.arg(m_format) <<
"No external template directory configured - will use embedded templates";
197 m_templateDir.clear();
199 m_emitStylesheet = config.get(m_format +
".stylesheet"_L1).asBool();
201 m_stylesheetName = config.get(m_format +
".stylesheetname"_L1).asString();
202 if (m_stylesheetName.isEmpty())
203 m_stylesheetName = u"qdoc-default.css"_s;
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;
221 HrefResolverConfig hrefConfig;
222 hrefConfig.context = &*m_context;
224 hrefConfig.cleanRefFn = [](
const QString &ref) {
return Generator::cleanRef(ref); };
225 hrefConfig.qmlTypeContextFn = []() ->
const QmlTypeNode * {
228 m_hrefResolver = std::make_unique<HrefResolver>(hrefConfig);
230 LinkResolverConfig linkConfig;
233 m_linkResolver = std::make_unique<LinkResolver>(&m_qdb, *m_hrefResolver, linkConfig);
235 m_catalogSource = std::make_unique<CatalogEntrySource>(
236 m_qdb, *m_hrefResolver, config.createInclusionPolicy());
238 IR::ListExpanderCallbacks callbacks;
239 callbacks.collectCppClasses =
240 [
this](
const Node *relative, Qt::SortOrder sortOrder) {
241 return m_catalogSource->collectCppClasses(relative, sortOrder);
243 callbacks.collectExamplesGrouped =
244 [
this](
const Node *relative) {
245 return m_catalogSource->collectExamplesGrouped(relative);
247 callbacks.collectCompactClasses =
248 [
this](
const Node *relative,
const QString &rootName) {
249 return m_catalogSource->collectCompactClasses(relative, rootName);
251 callbacks.collectGroupMembers =
252 [
this](
const Node *relative,
const QString &groupName,
253 Qt::SortOrder sortOrder) {
254 return m_catalogSource->collectGroupMembers(
255 relative, groupName, sortOrder);
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"
266 m_listExpander = std::make_unique<IR::ListExpander>(std::move(callbacks));
289 m_documentNames.append(outputFileName);
291 m_writer->beginDocument(outputFileName);
296 m_documentNames.removeLast();
298 m_writer->endDocument();
302
303
304
305
306
307
308
311 return m_documentNames.isEmpty() ? QString() : m_documentNames.constLast();
316 if (!node->url().isEmpty())
323 QFileInfo originalName(node->name());
324 QString suffix = originalName.suffix();
325 if (!suffix.isEmpty() && suffix !=
"html"_L1) {
327 QString name = fileBase(node);
328 return name +
'.'_L1 + suffix;
332 QString name = fileBase(node) +
'.'_L1;
333 return name + m_fileExtension;
340 IR::
PageMetadata pm = NodeExtractor::extractPageMetadata(cn, m_hrefResolver.get());
345 processDocumentBlocks(m_listExpander.get(), m_linkResolver.get(), ir, cn);
347 resolveImagePaths(ir);
348 renderDocument(ir,
"collection"_L1);
355 IR::
PageMetadata pm = NodeExtractor::extractPageMetadata(cn, m_hrefResolver.get());
360 processDocumentBlocks(m_listExpander.get(), m_linkResolver.get(), ir, cn);
362 resolveImagePaths(ir);
363 renderDocument(ir,
"collection"_L1);
370 IR::
PageMetadata pm = NodeExtractor::extractPageMetadata(pn, m_hrefResolver.get());
375 processDocumentBlocks(m_listExpander.get(), m_linkResolver.get(), ir, pn);
377 resolveImagePaths(ir);
378 renderDocument(ir,
"page"_L1);
387 NodeExtractor::SemanticOrigins origins;
388 IR::
PageMetadata pm = NodeExtractor::extractPageMetadata(aggregate, m_hrefResolver.get(),
396 ir.membersPageUrl = fileBase(aggregate) +
"-members."_L1 + m_fileExtension;
398 if (ir.cppReferenceInfo && ir.cppReferenceInfo->hasObsoleteMembers)
399 ir.cppReferenceInfo->obsoleteMembersUrl =
400 fileBase(aggregate) +
"-obsolete."_L1 + m_fileExtension;
402 processDocumentBlocks(m_listExpander.get(), m_linkResolver.get(), ir, aggregate, &origins);
404 resolveImagePaths(ir);
405 renderDocument(ir,
"cppref"_L1);
408 generateMemberListingPage(aggregate, *allMembers);
410 if (ir.cppReferenceInfo && ir.cppReferenceInfo->hasObsoleteMembers)
411 generateObsoleteMembersPage(aggregate);
420 NodeExtractor::SemanticOrigins origins;
421 IR::
PageMetadata pm = NodeExtractor::extractPageMetadata(qcn, m_hrefResolver.get(),
429 ir.membersPageUrl = fileBase(qcn) +
"-members."_L1 + m_fileExtension;
431 if (ir.qmlTypeInfo && ir.qmlTypeInfo->hasObsoleteMembers)
432 ir.qmlTypeInfo->obsoleteMembersUrl =
433 fileBase(qcn) +
"-obsolete."_L1 + m_fileExtension;
435 processDocumentBlocks(m_listExpander.get(), m_linkResolver.get(), ir, qcn, &origins);
437 resolveImagePaths(ir);
438 renderDocument(ir,
"qmltype"_L1);
441 generateMemberListingPage(qcn, *allMembers);
443 if (ir.qmlTypeInfo && ir.qmlTypeInfo->hasObsoleteMembers)
444 generateObsoleteMembersPage(qcn);
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);
467 return m_fileExtension;
471
472
473
474
475
476
479 const QString templateFileName = templateBaseName +
'.'_L1 + m_fileExtension;
480 const auto loaded = loadTemplate(m_templateDir, templateFileName);
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));
488 QJsonObject json = ir.toJson();
489 json[
"stylesheetEnabled"_L1] = m_emitStylesheet;
490 json[
"stylesheetName"_L1] = m_stylesheetName;
492 const InjaBridge::RenderContext context{ m_format, currentDocumentName(), loaded->path };
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);
500
501
502
503
504
505
506
507void TemplateGenerator::renderJson(
const QJsonObject &json,
const QString &templateBaseName)
509 const QString templateFileName = templateBaseName +
'.'_L1 + m_fileExtension;
510 const auto loaded = loadTemplate(m_templateDir, templateFileName);
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));
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;
524 const InjaBridge::RenderContext context{ m_format, currentDocumentName(), loaded->path };
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);
532
533
534
535
536
537
538
539
543 const QString membersFileName =
544 fileBase(node) +
"-members."_L1 + m_fileExtension;
546 QJsonObject json = allMembers.toJson();
547 json[
"title"_L1] = QString(
"List of All Members for "_L1 + allMembers.typeName);
549 beginDocument(membersFileName);
550 renderJson(json,
"members"_L1);
555
556
557
558
559
560
561
571 const QString obsoleteFileName =
572 fileBase(aggregate) +
"-obsolete."_L1 + m_fileExtension;
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);
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());
591 sectionJson[
"members"_L1] = membersJson;
592 summaryArr.append(sectionJson);
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());
606 sectionJson[
"members"_L1] = membersJson;
607 detailArr.append(sectionJson);
610 json[
"sections"_L1] = summaryArr;
611 json[
"detailSections"_L1] = detailArr;
613 beginDocument(obsoleteFileName);
614 renderJson(json,
"obsolete"_L1);
619
620
621
622
623
624
625
626
627
628
629
630std::optional<QString>
TemplateGenerator::resolveInclude(
const QString &name)
const
632 const auto loaded = loadTemplate(m_templateDir, name);
635 return loaded->content;
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
660 const NodeExtractor::SemanticOrigins *origins)
662 auto process = [&](QList<IR::ContentBlock> &blocks,
const Node *semantic) {
663 if (blocks.isEmpty())
666 expander->expand(blocks, page);
668 resolver->resolve(blocks, { semantic, page });
671 process(ir.body, page);
672 if (ir.cppReferenceInfo)
673 process(ir.cppReferenceInfo->threadSafetyAdmonition, page);
676 Q_ASSERT(!origins || origins->size() == ir.detailSections.size());
677 for (
int s = 0; s < ir.detailSections.size(); ++s) {
678 auto §ion = 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];
688 const Node *semantic = page;
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);
700
701
702
703
704
705
706
707
708
709
710
711
721 result = node->fileNameBase();
723 result = Utilities::computeFileBase(
724 node, m_context->project,
725 [
this](
const Node *n) -> QString {
726 if (n->isCollectionNode())
728 return m_context->outputPrefix(n->genus());
730 [
this](
const Node *n) {
return m_context->outputSuffix(n->genus()); });
731 const_cast<
Node *>(node)->setFileNameBase(result);
734 if (obsoleteFileBase)
735 result +=
"-obsolete"_L1;
740
741
742
743
744
745
746
747
748
749
753 const Config &config = Config::instance();
754 m_context.emplace(OutputContext::fromConfig(config, format()));
759 m_writer = std::make_unique<FileDocumentWriter>(*m_context);
763
764
765
766
767
768
769
770
771
772
778 const QString &outDir = m_context->outputDir.path();
779 if (outDir.isEmpty())
782 const QString imagesDir = u"images"_s;
783 const QString imagesDestDir = outDir +
'/'_L1 + imagesDir;
785 QDir().mkpath(imagesDestDir);
787 auto resolveInlines = [&](QList<IR::InlineContent> &inlines) {
788 for (
auto &inline_ : inlines) {
789 if (inline_.type != IR::InlineType::Image)
792 auto resolved = m_fileResolver.resolve(inline_.href);
796 const QString fileName = QFileInfo(resolved->get_path()).fileName();
797 QFile::copy(resolved->get_path(), imagesDestDir +
'/'_L1 + fileName);
798 inline_.href = imagesDir +
'/'_L1 + fileName;
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);
813 for (
auto §ion : ir.detailSections) {
814 for (
auto &member : section.members) {
815 walkBlocks(member.body);
816 walkBlocks(member.alsoList);
822
823
824
825
826
827
828
829
830
831
832
833
834
840 const QString &outDir = m_context->outputDir.path();
841 if (outDir.isEmpty())
844 QSet<QString> themeAssets;
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);
852 while (it.hasNext()) {
854 QString rel = assetsDir.relativeFilePath(it.filePath());
855 QString dst = outDir +
'/'_L1 + rel;
856 QDir().mkpath(QFileInfo(dst).path());
858 if (QFile::copy(it.filePath(), dst)) {
859 themeAssets.insert(rel);
860 qCDebug(lcQDocTemplateGenerator) <<
"[%1]"_L1.arg(m_format) <<
"Asset (theme):" << rel;
863 qCWarning(lcQDocTemplateGenerator)
864 <<
"[%1]"_L1.arg(m_format) <<
"Failed to copy asset:" << it.filePath() <<
"->" << dst;
868 qCDebug(lcQDocTemplateGenerator) <<
"[%1]"_L1.arg(m_format) <<
"Copied" << count <<
"theme asset(s)";
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;
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.
The Config class contains the configuration variables for controlling how qdoc produces documentation...
InclusionPolicy createInclusionPolicy() const
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()
static QmlTypeNode * qmlTypeContext()
static bool autolinkErrors()
Assembles IR Documents from pre-extracted metadata.
Document buildPageIR(PageMetadata pm) const
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.
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.
Sections(const Aggregate *aggregate)
This constructor builds the section vectors based on the type of the aggregate node.
bool hasObsoleteMembers(SectionPtrVector *summary_spv, SectionPtrVector *details_spv) const
Returns true if any sections in this object contain obsolete members.
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.
~TemplateGenerator() override
void generateGenericCollectionPage(CollectionNode *cn, CodeMarker *marker) override
Combined button and popup list for selecting options.
QList< const Section * > SectionPtrVector
Intermediate representation of the all-members listing page.
Intermediate representation for a documentation topic.
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.
virtual bool isPageNode() const
Returns true if this node represents something that generates a documentation page.
virtual bool isTextPageNode() const
Returns true if the node is a PageNode but not an Aggregate.
Aggregate * parent() const
Returns the node's parent pointer, that is, the aggregate under which the node is documented.
virtual bool isDeprecated() const
Returns true if this node's status is Deprecated.
virtual bool isAggregate() const
Returns true if this node is an aggregate, which means it inherits Aggregate and can therefore have c...
virtual bool isCollectionNode() const
Returns true if this is an instance of CollectionNode.
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)