16#include "ir/builder.h"
17#include "ir/document.h"
31#include <QtCore/qdir.h>
32#include <QtCore/qdiriterator.h>
33#include <QtCore/qfile.h>
34#include <QtCore/qfileinfo.h>
35#include <QtCore/qloggingcategory.h>
36#include <QtCore/qtextstream.h>
40Q_LOGGING_CATEGORY(lcQDocTemplateGenerator,
"qt.qdoc.templategenerator")
42using namespace Qt::Literals;
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
73 const QString &format)
74 : m_fileResolver(fileResolver)
88 createDefaultWriter();
90 const Config &config = Config::instance();
92 QString extensionConfig = config.get(m_format +
".extension"_L1).asString();
93 if (!extensionConfig.isEmpty())
94 m_fileExtension = extensionConfig;
96 QString templateDirConfig = config.get(m_format +
".templatedir"_L1).asString();
98 if (templateDirConfig.isEmpty()) {
99 m_templateDir.clear();
100 }
else if (QDir::isAbsolutePath(templateDirConfig)) {
101 m_templateDir = templateDirConfig;
105 m_templateDir = QDir::cleanPath(
106 QDir(config.currentDir()).absoluteFilePath(templateDirConfig));
109 bool foundTemplates =
false;
110 if (!m_templateDir.isEmpty()) {
111 QDir templateDir(m_templateDir);
112 if (templateDir.exists() && !templateDir.entryList(QDir::Files).isEmpty()) {
113 foundTemplates =
true;
114 qCInfo(lcQDocTemplateGenerator) <<
"Using template directory:" << m_templateDir;
115 }
else if (!templateDir.exists()) {
116 qCInfo(lcQDocTemplateGenerator)
117 <<
"Configured template directory does not exist:" << m_templateDir
118 <<
"- will use embedded templates";
120 qCInfo(lcQDocTemplateGenerator)
121 <<
"Configured template directory is empty:" << m_templateDir
122 <<
"- will use embedded templates";
125 qCInfo(lcQDocTemplateGenerator)
126 <<
"No external template directory configured - will use embedded templates";
130 m_templateDir.clear();
132 m_emitStylesheet = config.get(m_format +
".stylesheet"_L1).asBool();
134 m_stylesheetName = config.get(m_format +
".stylesheetname"_L1).asString();
135 if (m_stylesheetName.isEmpty())
136 m_stylesheetName = u"qdoc-default.css"_s;
142 if (m_stylesheetName.contains(
'/'_L1)
143 || m_stylesheetName.contains(
'\\'_L1)
144 || m_stylesheetName.contains(
".."_L1)
145 || QDir::isAbsolutePath(m_stylesheetName)) {
146 qCWarning(lcQDocTemplateGenerator)
147 <<
"Ignoring stylesheetname:" << m_stylesheetName
148 <<
"— must be a plain filename (no path separators)";
149 m_stylesheetName = u"qdoc-default.css"_s;
157 HrefResolverConfig hrefConfig;
158 hrefConfig.project = m_context->project;
159 hrefConfig.fileExtension = m_fileExtension;
160 hrefConfig.useOutputSubdirs = m_context->useSubdirs;
163 hrefConfig.outputPrefixFn = [
this](
const Node *node) {
164 return m_context->outputPrefix(nodeTypeKey(node));
166 hrefConfig.outputSuffixFn = [
this](
const Node *node) {
167 return m_context->outputSuffix(nodeTypeKey(node));
169 hrefConfig.cleanRefFn = [](
const QString &ref) {
return Generator::cleanRef(ref); };
170 hrefConfig.qmlTypeContextFn = []() ->
const QmlTypeNode * {
173 m_hrefResolver = std::make_unique<HrefResolver>(hrefConfig);
175 LinkResolverConfig linkConfig;
178 m_linkResolver = std::make_unique<LinkResolver>(&m_qdb, *m_hrefResolver, linkConfig);
202 m_writer->beginDocument(outputFileName);
208 m_writer->endDocument();
213 if (!node->url().isEmpty())
220 QFileInfo originalName(node->name());
221 QString suffix = originalName.suffix();
222 if (!suffix.isEmpty() && suffix !=
"html"_L1) {
224 QString name = fileBase(node);
225 return name +
'.'_L1 + suffix;
229 QString name = fileBase(node) +
'.'_L1;
230 return name + m_fileExtension;
243 resolveDocumentLinks(m_linkResolver.get(), ir, cn);
245 resolveImagePaths(ir);
246 renderDocument(ir,
"collection"_L1);
259 resolveDocumentLinks(m_linkResolver.get(), ir, cn);
261 resolveImagePaths(ir);
262 renderDocument(ir,
"collection"_L1);
275 resolveDocumentLinks(m_linkResolver.get(), ir, pn);
277 resolveImagePaths(ir);
278 renderDocument(ir,
"page"_L1);
286 if (m_writer && m_writer->isOpen()) {
287 m_writer->writeLine(QString(u"<!-- TemplateGenerator: C++ Reference Page for "_s
288 + aggregate->name() + u" -->"_s));
289 m_writer->writeLine(QString(u"<h1>"_s + aggregate->fullTitle() + u"</h1>"_s));
290 m_writer->writeLine(u"<p>Template-based output (IR integration pending)</p>"_s);
305 ir.membersPageUrl = fileBase(qcn) +
"-members."_L1 + m_fileExtension;
308 resolveDocumentLinks(m_linkResolver.get(), ir, qcn);
310 resolveImagePaths(ir);
311 renderDocument(ir,
"qmltype"_L1);
314 generateMemberListingPage(qcn, *allMembers);
322 if (m_writer && m_writer->isOpen()) {
323 m_writer->writeLine(QString(u"<!-- TemplateGenerator: Proxy Page for "_s
324 + aggregate->name() + u" -->"_s));
325 m_writer->writeLine(QString(u"<h1>"_s + aggregate->fullTitle() + u"</h1>"_s));
326 m_writer->writeLine(u"<p>Template-based output (IR integration pending)</p>"_s);
337 return m_fileExtension;
341
342
343
344
345
346
349 const QString templateFileName = templateBaseName +
'.'_L1 + m_fileExtension;
350 QString templateContent;
352 if (!m_templateDir.isEmpty()) {
353 QString templatePath = m_templateDir +
'/'_L1 + templateFileName;
354 QFile templateFile(templatePath);
356 if (templateFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
357 templateContent = QString::fromUtf8(templateFile.readAll());
358 templateFile.close();
362 if (templateContent.isEmpty()) {
363 QFile resourceFile(
":/qdoc/templates/"_L1 + templateFileName);
364 if (resourceFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
365 templateContent = QString::fromUtf8(resourceFile.readAll());
366 resourceFile.close();
370 if (templateContent.isEmpty())
371 qFatal(
"TemplateGenerator: No template file found for extension '%s'. "
372 "Ensure '%s.%s' exists in the configured template directory or in resources.",
373 qPrintable(m_fileExtension), qPrintable(templateBaseName),
374 qPrintable(m_fileExtension));
376 QJsonObject json = ir.toJson();
377 json[
"stylesheetEnabled"_L1] = m_emitStylesheet;
378 json[
"stylesheetName"_L1] = m_stylesheetName;
380 auto includeCallback = [
this](
const QString &name) {
return resolveInclude(name); };
381 QString rendered = InjaBridge::render(templateContent, json, includeCallback);
383 if (m_writer && m_writer->isOpen())
384 m_writer->write(rendered);
388
389
390
391
392
393
394
395void TemplateGenerator::renderJson(
const QJsonObject &json,
const QString &templateBaseName)
397 const QString templateFileName = templateBaseName +
'.'_L1 + m_fileExtension;
398 QString templateContent;
400 if (!m_templateDir.isEmpty()) {
401 QString templatePath = m_templateDir +
'/'_L1 + templateFileName;
402 QFile templateFile(templatePath);
404 if (templateFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
405 templateContent = QString::fromUtf8(templateFile.readAll());
406 templateFile.close();
410 if (templateContent.isEmpty()) {
411 QFile resourceFile(
":/qdoc/templates/"_L1 + templateFileName);
412 if (resourceFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
413 templateContent = QString::fromUtf8(resourceFile.readAll());
414 resourceFile.close();
418 if (templateContent.isEmpty())
419 qFatal(
"TemplateGenerator: No template file found for '%s'. "
420 "Ensure '%s.%s' exists in the configured template directory or in resources.",
421 qPrintable(templateBaseName), qPrintable(templateBaseName),
422 qPrintable(m_fileExtension));
424 QJsonObject enrichedJson = json;
425 enrichedJson[
"stylesheetEnabled"_L1] = m_emitStylesheet;
426 enrichedJson[
"stylesheetName"_L1] = m_stylesheetName;
428 auto includeCallback = [
this](
const QString &name) {
return resolveInclude(name); };
429 QString rendered = InjaBridge::render(templateContent, enrichedJson, includeCallback);
431 if (m_writer && m_writer->isOpen())
432 m_writer->write(rendered);
436
437
438
439
440
441
442
443
447 const QString membersFileName =
448 fileBase(node) +
"-members."_L1 + m_fileExtension;
450 QJsonObject json = allMembers.toJson();
451 json[
"title"_L1] = QString(
"List of All Members for "_L1 + allMembers.typeName);
453 beginDocument(membersFileName);
454 renderJson(json,
"members"_L1);
459
460
461
462
463
464
465
466
467
468
469
472 if (!m_templateDir.isEmpty()) {
473 QFile file(m_templateDir +
'/'_L1 + name);
474 if (file.open(QIODevice::ReadOnly | QIODevice::Text))
475 return QString::fromUtf8(file.readAll());
478 QFile resourceFile(
":/qdoc/templates/"_L1 + name);
479 if (resourceFile.open(QIODevice::ReadOnly | QIODevice::Text))
480 return QString::fromUtf8(resourceFile.readAll());
486
487
488
489
490
508 if (!ir.body.isEmpty())
509 resolver->resolve(ir.body, relative);
510 for (
auto §ion : ir.detailSections) {
511 for (
auto &member : section.members) {
512 if (!member.body.isEmpty())
513 resolver->resolve(member.body, relative);
514 if (!member.alsoList.isEmpty())
515 resolver->resolve(member.alsoList, relative);
521
522
523
524
525
526
527
534 return node->fileNameBase();
536 QString result = Utilities::computeFileBase(
537 node, m_context->project,
538 [
this](
const Node *n) -> QString {
539 if (n->isCollectionNode())
541 return m_context->outputPrefix(nodeTypeKey(n));
543 [
this](
const Node *n) {
return m_context->outputSuffix(nodeTypeKey(n)); });
545 const_cast<
Node *>(node)->setFileNameBase(result);
550
551
552
553
554
555
556
557
558
559
563 const Config &config = Config::instance();
564 m_context.emplace(OutputContext::fromConfig(config, format()));
569 m_writer = std::make_unique<FileDocumentWriter>(*m_context);
573
574
575
576
577
578
579
580
581
582
588 const QString &outDir = m_context->outputDir.path();
589 if (outDir.isEmpty())
592 const QString imagesDir = u"images"_s;
593 const QString imagesDestDir = outDir +
'/'_L1 + imagesDir;
595 QDir().mkpath(imagesDestDir);
597 auto resolveInlines = [&](QList<IR::InlineContent> &inlines) {
598 for (
auto &inline_ : inlines) {
599 if (inline_.type != IR::InlineType::Image)
602 auto resolved = m_fileResolver.resolve(inline_.href);
606 const QString fileName = QFileInfo(resolved->get_path()).fileName();
607 QFile::copy(resolved->get_path(), imagesDestDir +
'/'_L1 + fileName);
608 inline_.href = imagesDir +
'/'_L1 + fileName;
612 std::function<
void(QList<IR::ContentBlock> &)> walkBlocks;
613 walkBlocks = [&](QList<IR::ContentBlock> &blocks) {
614 for (
auto &block : blocks) {
615 resolveInlines(block.inlineContent);
616 if (!block.children.isEmpty())
617 walkBlocks(block.children);
623 for (
auto §ion : ir.detailSections) {
624 for (
auto &member : section.members) {
625 walkBlocks(member.body);
626 walkBlocks(member.alsoList);
632
633
634
635
636
637
638
639
640
641
642
643
644
650 const QString &outDir = m_context->outputDir.path();
651 if (outDir.isEmpty())
654 QSet<QString> themeAssets;
656 if (!m_templateDir.isEmpty()) {
657 QDir assetsDir(m_templateDir +
"/assets"_L1);
658 if (assetsDir.exists()) {
659 QDirIterator it(assetsDir.path(), QDir::Files,
660 QDirIterator::Subdirectories);
662 while (it.hasNext()) {
664 QString rel = assetsDir.relativeFilePath(it.filePath());
665 QString dst = outDir +
'/'_L1 + rel;
666 QDir().mkpath(QFileInfo(dst).path());
668 if (QFile::copy(it.filePath(), dst)) {
669 themeAssets.insert(rel);
670 qCDebug(lcQDocTemplateGenerator) <<
"Asset (theme):" << rel;
673 qCWarning(lcQDocTemplateGenerator)
674 <<
"Failed to copy asset:" << it.filePath() <<
"->" << dst;
678 qCInfo(lcQDocTemplateGenerator) <<
"Copied" << count <<
"theme asset(s)";
682 if (m_emitStylesheet && !themeAssets.contains(m_stylesheetName)) {
683 const QString dstCss = outDir +
'/'_L1 + m_stylesheetName;
684 QFile::remove(dstCss);
685 QFile::copy(
":/qdoc/templates/assets/qdoc-default.css"_L1, dstCss);
686 QFile(dstCss).setPermissions(QFile::ReadOwner | QFile::WriteOwner
687 | QFile::ReadGroup | QFile::ReadOther);
688 qCDebug(lcQDocTemplateGenerator) <<
"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...
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.
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)