17#include "ir/builder.h"
18#include "ir/document.h"
19#include "ir/listexpander.h"
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>
43Q_LOGGING_CATEGORY(lcQDocTemplateGenerator,
"qt.qdoc.templategenerator")
45using namespace Qt::Literals;
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
76 const QString &format)
77 : m_fileResolver(fileResolver)
91 createDefaultWriter();
93 const Config &config = Config::instance();
95 QString extensionConfig = config.get(m_format +
".extension"_L1).asString();
96 if (!extensionConfig.isEmpty())
97 m_fileExtension = extensionConfig;
102 m_context->fileExtension = m_fileExtension;
104 const ConfigVar &templateDirVar = config.get(m_format +
".templatedir"_L1);
105 QString templateDirConfig = templateDirVar.asString();
107 if (templateDirConfig.isEmpty()) {
108 m_templateDir.clear();
109 }
else if (QDir::isAbsolutePath(templateDirConfig)) {
110 m_templateDir = templateDirConfig;
123 const QString base = templateDirVar.path().isEmpty()
124 ? config.currentDir()
125 : templateDirVar.path();
126 m_templateDir = QDir::cleanPath(QDir(base).absoluteFilePath(templateDirConfig));
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";
140 qCInfo(lcQDocTemplateGenerator)
141 <<
"[%1]"_L1.arg(m_format) <<
"Configured template directory is empty:" << m_templateDir
142 <<
"- will use embedded templates";
145 qCDebug(lcQDocTemplateGenerator)
146 <<
"[%1]"_L1.arg(m_format) <<
"No external template directory configured - will use embedded templates";
150 m_templateDir.clear();
152 m_emitStylesheet = config.get(m_format +
".stylesheet"_L1).asBool();
154 m_stylesheetName = config.get(m_format +
".stylesheetname"_L1).asString();
155 if (m_stylesheetName.isEmpty())
156 m_stylesheetName = u"qdoc-default.css"_s;
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;
174 HrefResolverConfig hrefConfig;
175 hrefConfig.context = &*m_context;
177 hrefConfig.cleanRefFn = [](
const QString &ref) {
return Generator::cleanRef(ref); };
178 hrefConfig.qmlTypeContextFn = []() ->
const QmlTypeNode * {
181 m_hrefResolver = std::make_unique<HrefResolver>(hrefConfig);
183 LinkResolverConfig linkConfig;
186 m_linkResolver = std::make_unique<LinkResolver>(&m_qdb, *m_hrefResolver, linkConfig);
188 m_catalogSource = std::make_unique<CatalogEntrySource>(
189 m_qdb, *m_hrefResolver, config.createInclusionPolicy());
191 IR::ListExpanderCallbacks callbacks;
192 callbacks.collectCppClasses =
193 [
this](
const Node *relative, Qt::SortOrder sortOrder) {
194 return m_catalogSource->collectCppClasses(relative, sortOrder);
196 callbacks.collectExamplesGrouped =
197 [
this](
const Node *relative) {
198 return m_catalogSource->collectExamplesGrouped(relative);
200 callbacks.collectCompactClasses =
201 [
this](
const Node *relative,
const QString &rootName) {
202 return m_catalogSource->collectCompactClasses(relative, rootName);
204 callbacks.collectGroupMembers =
205 [
this](
const Node *relative,
const QString &groupName,
206 Qt::SortOrder sortOrder) {
207 return m_catalogSource->collectGroupMembers(
208 relative, groupName, sortOrder);
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"
219 m_listExpander = std::make_unique<IR::ListExpander>(std::move(callbacks));
243 m_writer->beginDocument(outputFileName);
249 m_writer->endDocument();
254 if (!node->url().isEmpty())
261 QFileInfo originalName(node->name());
262 QString suffix = originalName.suffix();
263 if (!suffix.isEmpty() && suffix !=
"html"_L1) {
265 QString name = fileBase(node);
266 return name +
'.'_L1 + suffix;
270 QString name = fileBase(node) +
'.'_L1;
271 return name + m_fileExtension;
283 processDocumentBlocks(m_listExpander.get(), m_linkResolver.get(), ir, cn);
285 resolveImagePaths(ir);
286 renderDocument(ir,
"collection"_L1);
298 processDocumentBlocks(m_listExpander.get(), m_linkResolver.get(), ir, cn);
300 resolveImagePaths(ir);
301 renderDocument(ir,
"collection"_L1);
313 processDocumentBlocks(m_listExpander.get(), m_linkResolver.get(), ir, pn);
315 resolveImagePaths(ir);
316 renderDocument(ir,
"page"_L1);
330 ir.membersPageUrl = fileBase(aggregate) +
"-members."_L1 + m_fileExtension;
332 if (ir.cppReferenceInfo && ir.cppReferenceInfo->hasObsoleteMembers)
333 ir.cppReferenceInfo->obsoleteMembersUrl =
334 fileBase(aggregate) +
"-obsolete."_L1 + m_fileExtension;
336 processDocumentBlocks(m_listExpander.get(), m_linkResolver.get(), ir, aggregate);
338 resolveImagePaths(ir);
339 renderDocument(ir,
"cppref"_L1);
342 generateMemberListingPage(aggregate, *allMembers);
344 if (ir.cppReferenceInfo && ir.cppReferenceInfo->hasObsoleteMembers)
345 generateObsoleteMembersPage(aggregate);
359 ir.membersPageUrl = fileBase(qcn) +
"-members."_L1 + m_fileExtension;
361 processDocumentBlocks(m_listExpander.get(), m_linkResolver.get(), ir, qcn);
363 resolveImagePaths(ir);
364 renderDocument(ir,
"qmltype"_L1);
367 generateMemberListingPage(qcn, *allMembers);
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);
390 return m_fileExtension;
394
395
396
397
398
399
402 const QString templateFileName = templateBaseName +
'.'_L1 + m_fileExtension;
403 QString templateContent;
405 if (!m_templateDir.isEmpty()) {
406 QString templatePath = m_templateDir +
'/'_L1 + templateFileName;
407 QFile templateFile(templatePath);
409 if (templateFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
410 templateContent = QString::fromUtf8(templateFile.readAll());
411 templateFile.close();
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();
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));
429 QJsonObject json = ir.toJson();
430 json[
"stylesheetEnabled"_L1] = m_emitStylesheet;
431 json[
"stylesheetName"_L1] = m_stylesheetName;
433 auto includeCallback = [
this](
const QString &name) {
return resolveInclude(name); };
434 QString rendered = InjaBridge::render(templateContent, json, includeCallback);
436 if (m_writer && m_writer->isOpen())
437 m_writer->write(rendered);
441
442
443
444
445
446
447
448void TemplateGenerator::renderJson(
const QJsonObject &json,
const QString &templateBaseName)
450 const QString templateFileName = templateBaseName +
'.'_L1 + m_fileExtension;
451 QString templateContent;
453 if (!m_templateDir.isEmpty()) {
454 QString templatePath = m_templateDir +
'/'_L1 + templateFileName;
455 QFile templateFile(templatePath);
457 if (templateFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
458 templateContent = QString::fromUtf8(templateFile.readAll());
459 templateFile.close();
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();
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));
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;
483 auto includeCallback = [
this](
const QString &name) {
return resolveInclude(name); };
484 QString rendered = InjaBridge::render(templateContent, enrichedJson, includeCallback);
486 if (m_writer && m_writer->isOpen())
487 m_writer->write(rendered);
491
492
493
494
495
496
497
498
502 const QString membersFileName =
503 fileBase(node) +
"-members."_L1 + m_fileExtension;
505 QJsonObject json = allMembers.toJson();
506 json[
"title"_L1] = QString(
"List of All Members for "_L1 + allMembers.typeName);
508 beginDocument(membersFileName);
509 renderJson(json,
"members"_L1);
514
515
516
517
518
519
520
530 const QString obsoleteFileName =
531 fileBase(aggregate) +
"-obsolete."_L1 + m_fileExtension;
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);
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());
550 sectionJson[
"members"_L1] = membersJson;
551 summaryArr.append(sectionJson);
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());
565 sectionJson[
"members"_L1] = membersJson;
566 detailArr.append(sectionJson);
569 json[
"sections"_L1] = summaryArr;
570 json[
"detailSections"_L1] = detailArr;
572 beginDocument(obsoleteFileName);
573 renderJson(json,
"obsolete"_L1);
578
579
580
581
582
583
584
585
586
587
588
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());
597 QFile resourceFile(
":/qdoc/templates/"_L1 + name);
598 if (resourceFile.open(QIODevice::ReadOnly | QIODevice::Text))
599 return QString::fromUtf8(resourceFile.readAll());
616 auto process = [&](QList<IR::ContentBlock> &blocks) {
617 if (blocks.isEmpty())
620 expander->expand(blocks, relative);
622 resolver->resolve(blocks, relative);
626 if (ir.cppReferenceInfo)
627 process(ir.cppReferenceInfo->threadSafetyAdmonition);
628 for (
auto §ion : ir.detailSections) {
629 for (
auto &member : section.members) {
630 process(member.body);
631 process(member.alsoList);
637
638
639
640
641
642
643
650 return node->fileNameBase();
652 QString result = Utilities::computeFileBase(
653 node, m_context->project,
654 [
this](
const Node *n) -> QString {
655 if (n->isCollectionNode())
657 return m_context->outputPrefix(n->genus());
659 [
this](
const Node *n) {
return m_context->outputSuffix(n->genus()); });
661 const_cast<
Node *>(node)->setFileNameBase(result);
666
667
668
669
670
671
672
673
674
675
679 const Config &config = Config::instance();
680 m_context.emplace(OutputContext::fromConfig(config, format()));
685 m_writer = std::make_unique<FileDocumentWriter>(*m_context);
689
690
691
692
693
694
695
696
697
698
704 const QString &outDir = m_context->outputDir.path();
705 if (outDir.isEmpty())
708 const QString imagesDir = u"images"_s;
709 const QString imagesDestDir = outDir +
'/'_L1 + imagesDir;
711 QDir().mkpath(imagesDestDir);
713 auto resolveInlines = [&](QList<IR::InlineContent> &inlines) {
714 for (
auto &inline_ : inlines) {
715 if (inline_.type != IR::InlineType::Image)
718 auto resolved = m_fileResolver.resolve(inline_.href);
722 const QString fileName = QFileInfo(resolved->get_path()).fileName();
723 QFile::copy(resolved->get_path(), imagesDestDir +
'/'_L1 + fileName);
724 inline_.href = imagesDir +
'/'_L1 + fileName;
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);
739 for (
auto §ion : ir.detailSections) {
740 for (
auto &member : section.members) {
741 walkBlocks(member.body);
742 walkBlocks(member.alsoList);
748
749
750
751
752
753
754
755
756
757
758
759
760
766 const QString &outDir = m_context->outputDir.path();
767 if (outDir.isEmpty())
770 QSet<QString> themeAssets;
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);
778 while (it.hasNext()) {
780 QString rel = assetsDir.relativeFilePath(it.filePath());
781 QString dst = outDir +
'/'_L1 + rel;
782 QDir().mkpath(QFileInfo(dst).path());
784 if (QFile::copy(it.filePath(), dst)) {
785 themeAssets.insert(rel);
786 qCDebug(lcQDocTemplateGenerator) <<
"[%1]"_L1.arg(m_format) <<
"Asset (theme):" << rel;
789 qCWarning(lcQDocTemplateGenerator)
790 <<
"[%1]"_L1.arg(m_format) <<
"Failed to copy asset:" << it.filePath() <<
"->" << dst;
794 qCDebug(lcQDocTemplateGenerator) <<
"[%1]"_L1.arg(m_format) <<
"Copied" << count <<
"theme asset(s)";
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;
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
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
~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.
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 *relative)