16#include "ir/builder.h"
17#include "ir/document.h"
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>
41Q_LOGGING_CATEGORY(lcQDocTemplateGenerator,
"qt.qdoc.templategenerator")
43using namespace Qt::Literals;
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
74 const QString &format)
75 : m_fileResolver(fileResolver)
89 createDefaultWriter();
91 const Config &config = Config::instance();
93 QString extensionConfig = config.get(m_format +
".extension"_L1).asString();
94 if (!extensionConfig.isEmpty())
95 m_fileExtension = extensionConfig;
97 QString templateDirConfig = config.get(m_format +
".templatedir"_L1).asString();
99 if (templateDirConfig.isEmpty()) {
100 m_templateDir.clear();
101 }
else if (QDir::isAbsolutePath(templateDirConfig)) {
102 m_templateDir = templateDirConfig;
106 m_templateDir = QDir::cleanPath(
107 QDir(config.currentDir()).absoluteFilePath(templateDirConfig));
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";
121 qCInfo(lcQDocTemplateGenerator)
122 <<
"[%1]"_L1.arg(m_format) <<
"Configured template directory is empty:" << m_templateDir
123 <<
"- will use embedded templates";
126 qCInfo(lcQDocTemplateGenerator)
127 <<
"[%1]"_L1.arg(m_format) <<
"No external template directory configured - will use embedded templates";
131 m_templateDir.clear();
133 m_emitStylesheet = config.get(m_format +
".stylesheet"_L1).asBool();
135 m_stylesheetName = config.get(m_format +
".stylesheetname"_L1).asString();
136 if (m_stylesheetName.isEmpty())
137 m_stylesheetName = u"qdoc-default.css"_s;
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;
158 HrefResolverConfig hrefConfig;
159 hrefConfig.project = m_context->project;
160 hrefConfig.fileExtension = m_fileExtension;
161 hrefConfig.useOutputSubdirs = m_context->useSubdirs;
164 hrefConfig.outputPrefixFn = [
this](
const Node *node) {
165 return m_context->outputPrefix(nodeTypeKey(node));
167 hrefConfig.outputSuffixFn = [
this](
const Node *node) {
168 return m_context->outputSuffix(nodeTypeKey(node));
170 hrefConfig.cleanRefFn = [](
const QString &ref) {
return Generator::cleanRef(ref); };
171 hrefConfig.qmlTypeContextFn = []() ->
const QmlTypeNode * {
174 m_hrefResolver = std::make_unique<HrefResolver>(hrefConfig);
176 LinkResolverConfig linkConfig;
179 m_linkResolver = std::make_unique<LinkResolver>(&m_qdb, *m_hrefResolver, linkConfig);
203 m_writer->beginDocument(outputFileName);
209 m_writer->endDocument();
214 if (!node->url().isEmpty())
221 QFileInfo originalName(node->name());
222 QString suffix = originalName.suffix();
223 if (!suffix.isEmpty() && suffix !=
"html"_L1) {
225 QString name = fileBase(node);
226 return name +
'.'_L1 + suffix;
230 QString name = fileBase(node) +
'.'_L1;
231 return name + m_fileExtension;
244 resolveDocumentLinks(m_linkResolver.get(), ir, cn);
246 resolveImagePaths(ir);
247 renderDocument(ir,
"collection"_L1);
260 resolveDocumentLinks(m_linkResolver.get(), ir, cn);
262 resolveImagePaths(ir);
263 renderDocument(ir,
"collection"_L1);
276 resolveDocumentLinks(m_linkResolver.get(), ir, pn);
278 resolveImagePaths(ir);
279 renderDocument(ir,
"page"_L1);
293 ir.membersPageUrl = fileBase(aggregate) +
"-members."_L1 + m_fileExtension;
295 if (ir.cppReferenceInfo && ir.cppReferenceInfo->hasObsoleteMembers)
296 ir.cppReferenceInfo->obsoleteMembersUrl =
297 fileBase(aggregate) +
"-obsolete."_L1 + m_fileExtension;
300 resolveDocumentLinks(m_linkResolver.get(), ir, aggregate);
302 resolveImagePaths(ir);
303 renderDocument(ir,
"cppref"_L1);
306 generateMemberListingPage(aggregate, *allMembers);
308 if (ir.cppReferenceInfo && ir.cppReferenceInfo->hasObsoleteMembers)
309 generateObsoleteMembersPage(aggregate);
323 ir.membersPageUrl = fileBase(qcn) +
"-members."_L1 + m_fileExtension;
326 resolveDocumentLinks(m_linkResolver.get(), ir, qcn);
328 resolveImagePaths(ir);
329 renderDocument(ir,
"qmltype"_L1);
332 generateMemberListingPage(qcn, *allMembers);
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);
355 return m_fileExtension;
359
360
361
362
363
364
367 const QString templateFileName = templateBaseName +
'.'_L1 + m_fileExtension;
368 QString templateContent;
370 if (!m_templateDir.isEmpty()) {
371 QString templatePath = m_templateDir +
'/'_L1 + templateFileName;
372 QFile templateFile(templatePath);
374 if (templateFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
375 templateContent = QString::fromUtf8(templateFile.readAll());
376 templateFile.close();
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();
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));
394 QJsonObject json = ir.toJson();
395 json[
"stylesheetEnabled"_L1] = m_emitStylesheet;
396 json[
"stylesheetName"_L1] = m_stylesheetName;
398 auto includeCallback = [
this](
const QString &name) {
return resolveInclude(name); };
399 QString rendered = InjaBridge::render(templateContent, json, includeCallback);
401 if (m_writer && m_writer->isOpen())
402 m_writer->write(rendered);
406
407
408
409
410
411
412
413void TemplateGenerator::renderJson(
const QJsonObject &json,
const QString &templateBaseName)
415 const QString templateFileName = templateBaseName +
'.'_L1 + m_fileExtension;
416 QString templateContent;
418 if (!m_templateDir.isEmpty()) {
419 QString templatePath = m_templateDir +
'/'_L1 + templateFileName;
420 QFile templateFile(templatePath);
422 if (templateFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
423 templateContent = QString::fromUtf8(templateFile.readAll());
424 templateFile.close();
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();
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));
442 QJsonObject enrichedJson = json;
443 enrichedJson[
"stylesheetEnabled"_L1] = m_emitStylesheet;
444 enrichedJson[
"stylesheetName"_L1] = m_stylesheetName;
446 auto includeCallback = [
this](
const QString &name) {
return resolveInclude(name); };
447 QString rendered = InjaBridge::render(templateContent, enrichedJson, includeCallback);
449 if (m_writer && m_writer->isOpen())
450 m_writer->write(rendered);
454
455
456
457
458
459
460
461
465 const QString membersFileName =
466 fileBase(node) +
"-members."_L1 + m_fileExtension;
468 QJsonObject json = allMembers.toJson();
469 json[
"title"_L1] = QString(
"List of All Members for "_L1 + allMembers.typeName);
471 beginDocument(membersFileName);
472 renderJson(json,
"members"_L1);
477
478
479
480
481
482
483
493 const QString obsoleteFileName =
494 fileBase(aggregate) +
"-obsolete."_L1 + m_fileExtension;
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);
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());
513 sectionJson[
"members"_L1] = membersJson;
514 summaryArr.append(sectionJson);
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());
528 sectionJson[
"members"_L1] = membersJson;
529 detailArr.append(sectionJson);
532 json[
"sections"_L1] = summaryArr;
533 json[
"detailSections"_L1] = detailArr;
535 beginDocument(obsoleteFileName);
536 renderJson(json,
"obsolete"_L1);
541
542
543
544
545
546
547
548
549
550
551
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());
560 QFile resourceFile(
":/qdoc/templates/"_L1 + name);
561 if (resourceFile.open(QIODevice::ReadOnly | QIODevice::Text))
562 return QString::fromUtf8(resourceFile.readAll());
568
569
570
571
572
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 §ion : 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);
605
606
607
608
609
610
611
618 return node->fileNameBase();
620 QString result = Utilities::computeFileBase(
621 node, m_context->project,
622 [
this](
const Node *n) -> QString {
623 if (n->isCollectionNode())
625 return m_context->outputPrefix(nodeTypeKey(n));
627 [
this](
const Node *n) {
return m_context->outputSuffix(nodeTypeKey(n)); });
629 const_cast<
Node *>(node)->setFileNameBase(result);
634
635
636
637
638
639
640
641
642
643
647 const Config &config = Config::instance();
648 m_context.emplace(OutputContext::fromConfig(config, format()));
653 m_writer = std::make_unique<FileDocumentWriter>(*m_context);
657
658
659
660
661
662
663
664
665
666
672 const QString &outDir = m_context->outputDir.path();
673 if (outDir.isEmpty())
676 const QString imagesDir = u"images"_s;
677 const QString imagesDestDir = outDir +
'/'_L1 + imagesDir;
679 QDir().mkpath(imagesDestDir);
681 auto resolveInlines = [&](QList<IR::InlineContent> &inlines) {
682 for (
auto &inline_ : inlines) {
683 if (inline_.type != IR::InlineType::Image)
686 auto resolved = m_fileResolver.resolve(inline_.href);
690 const QString fileName = QFileInfo(resolved->get_path()).fileName();
691 QFile::copy(resolved->get_path(), imagesDestDir +
'/'_L1 + fileName);
692 inline_.href = imagesDir +
'/'_L1 + fileName;
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);
707 for (
auto §ion : ir.detailSections) {
708 for (
auto &member : section.members) {
709 walkBlocks(member.body);
710 walkBlocks(member.alsoList);
716
717
718
719
720
721
722
723
724
725
726
727
728
734 const QString &outDir = m_context->outputDir.path();
735 if (outDir.isEmpty())
738 QSet<QString> themeAssets;
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);
746 while (it.hasNext()) {
748 QString rel = assetsDir.relativeFilePath(it.filePath());
749 QString dst = outDir +
'/'_L1 + rel;
750 QDir().mkpath(QFileInfo(dst).path());
752 if (QFile::copy(it.filePath(), dst)) {
753 themeAssets.insert(rel);
754 qCDebug(lcQDocTemplateGenerator) <<
"[%1]"_L1.arg(m_format) <<
"Asset (theme):" << rel;
757 qCWarning(lcQDocTemplateGenerator)
758 <<
"[%1]"_L1.arg(m_format) <<
"Failed to copy asset:" << it.filePath() <<
"->" << dst;
762 qCInfo(lcQDocTemplateGenerator) <<
"[%1]"_L1.arg(m_format) <<
"Copied" << count <<
"theme asset(s)";
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;
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...
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.
Genus genus() const override
Returns this node's Genus.
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 QString nodeTypeKey(const Node *node)
static void resolveDocumentLinks(LinkResolver *resolver, IR::Document &ir, const Node *relative)