1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include "generator.h"
5
6#include "access.h"
7#include "aggregate.h"
8#include "classnode.h"
9#include "codemarker.h"
10#include "collectionnode.h"
11#include "comparisoncategory.h"
12#include "config.h"
13#include "doc.h"
14#include "editdistance.h"
15#include "enumnode.h"
16#include "examplenode.h"
17#include "functionnode.h"
18#include "inode.h"
19#include "node.h"
20#include "openedlist.h"
21#include "propertynode.h"
22#include "qdocdatabase.h"
23#include "qmltypenode.h"
24#include "qmlpropertynode.h"
25#include "quoter.h"
26#include "sharedcommentnode.h"
27#include "tokenizer.h"
28#include "typedefnode.h"
29#include "utilities.h"
30
31#include <QtCore/qdebug.h>
32#include <QtCore/qdir.h>
33#include <QtCore/qregularexpression.h>
34
35#ifndef QT_BOOTSTRAPPED
36# include "QtCore/qurl.h"
37#endif
38
39#include <string>
40
41using namespace std::literals::string_literals;
42
43QT_BEGIN_NAMESPACE
44
45using namespace Qt::StringLiterals;
46
47Generator *Generator::s_currentGenerator;
48QMap<QString, QMap<QString, QString>> Generator::s_fmtLeftMaps;
49QMap<QString, QMap<QString, QString>> Generator::s_fmtRightMaps;
50QList<Generator *> Generator::s_generators;
51QString Generator::s_outDir;
52QString Generator::s_outSubdir;
53QStringList Generator::s_outFileNames;
54QSet<QString> Generator::s_trademarks;
55QSet<QString> Generator::s_outputFormats;
56QHash<QString, QString> Generator::s_outputPrefixes;
57QHash<QString, QString> Generator::s_outputSuffixes;
58QString Generator::s_project;
59bool Generator::s_noLinkErrors = false;
60bool Generator::s_autolinkErrors = false;
61bool Generator::s_redirectDocumentationToDevNull = false;
62bool Generator::s_useOutputSubdirs = true;
63QmlTypeNode *Generator::s_qmlTypeContext = nullptr;
64
65static QRegularExpression tag("</?@[^>]*>");
66static QLatin1String amp("&amp;");
67static QLatin1String gt("&gt;");
68static QLatin1String lt("&lt;");
69static QLatin1String quot("&quot;");
70
71/*!
72 Constructs the generator base class. Prepends the newly
73 constructed generator to the list of output generators.
74 Sets a pointer to the QDoc database singleton, which is
75 available to the generator subclasses.
76 */
77Generator::Generator(FileResolver& file_resolver)
78 : file_resolver{file_resolver}
79{
80 m_qdb = QDocDatabase::qdocDB();
81 s_generators.prepend(t: this);
82}
83
84/*!
85 Destroys the generator after removing it from the list of
86 output generators.
87 */
88Generator::~Generator()
89{
90 s_generators.removeAll(t: this);
91}
92
93void Generator::appendFullName(Text &text, const Node *apparentNode, const Node *relative,
94 const Node *actualNode)
95{
96 if (actualNode == nullptr)
97 actualNode = apparentNode;
98
99 addNodeLink(text, node: actualNode, linkText: apparentNode->plainFullName(relative));
100}
101
102void Generator::appendFullName(Text &text, const Node *apparentNode, const QString &fullName,
103 const Node *actualNode)
104{
105 if (actualNode == nullptr)
106 actualNode = apparentNode;
107
108 addNodeLink(text, node: actualNode, linkText: fullName);
109}
110
111/*!
112 Append the signature for the function named in \a node to
113 \a text, so that is a link to the documentation for that
114 function.
115 */
116void Generator::appendSignature(Text &text, const Node *node)
117{
118 addNodeLink(text, node, linkText: node->signature(Node::SignaturePlain));
119}
120
121/*!
122 Generate a bullet list of function signatures. The function
123 nodes are in \a nodes. It uses the \a relative node and the
124 \a marker for the generation.
125 */
126void Generator::signatureList(const NodeList &nodes, const Node *relative, CodeMarker *marker)
127{
128 Text text;
129 int count = 0;
130 text << Atom(Atom::ListLeft, QString("bullet"));
131 for (const auto &node : nodes) {
132 text << Atom(Atom::ListItemNumber, QString::number(++count));
133 text << Atom(Atom::ListItemLeft, QString("bullet"));
134 appendSignature(text, node);
135 text << Atom(Atom::ListItemRight, QString("bullet"));
136 }
137 text << Atom(Atom::ListRight, QString("bullet"));
138 generateText(text, relative, marker);
139}
140
141int Generator::appendSortedNames(Text &text, const ClassNode *cn, const QList<RelatedClass> &rc)
142{
143 QMap<QString, Text> classMap;
144 for (const auto &relatedClass : rc) {
145 ClassNode *rcn = relatedClass.m_node;
146 if (rcn && rcn->isInAPI()) {
147 Text className;
148 appendFullName(text&: className, apparentNode: rcn, relative: cn);
149 classMap[className.toString().toLower()] = className;
150 }
151 }
152
153 int index = 0;
154 const QStringList classNames = classMap.keys();
155 for (const auto &className : classNames) {
156 text << classMap[className];
157 text << Utilities::comma(wordPosition: index++, numberOfWords: classNames.size());
158 }
159 return index;
160}
161
162int Generator::appendSortedQmlNames(Text &text, const Node *base, const QStringList &knownTypes,
163 const NodeList &subs)
164{
165 QMap<QString, Text> classMap;
166
167 QStringList typeNames(knownTypes);
168 for (const auto sub : subs)
169 typeNames << sub->name();
170
171 for (const auto sub : subs) {
172 Text full_name;
173 appendFullName(text&: full_name, apparentNode: sub, relative: base);
174 // Disambiguate with '(<QML module name>)' if there are clashing type names
175 if (typeNames.count(t: sub->name()) > 1)
176 full_name << Atom(Atom::String, " (%1)"_L1.arg(args: sub->logicalModuleName()));
177 classMap[full_name.toString().toLower()] = full_name;
178 }
179
180 int index = 0;
181 const auto &names = classMap.keys();
182 for (const auto &name : names)
183 text << classMap[name] << Utilities::comma(wordPosition: index++, numberOfWords: names.size());
184 return index;
185}
186
187/*!
188 Creates the file named \a fileName in the output directory
189 and returns a QFile pointing to this file. In particular,
190 this method deals with errors when opening the file:
191 the returned QFile is always valid and can be written to.
192
193 \sa beginSubPage()
194 */
195QFile *Generator::openSubPageFile(const PageNode *node, const QString &fileName)
196{
197 // Skip generating a warning for license attribution pages, as their source
198 // is generated by qtattributionsscanner and may potentially include duplicates.
199 // NOTE: Depending on the value of the `QtParts` field in qt_attribution.json files,
200 // qtattributionsscanner may not use the \attribution QDoc command for the page
201 // (by design). Therefore, check also filename.
202 if (s_outFileNames.contains(str: fileName) && !node->isAttribution() && !fileName.contains(s: "-attribution-"_L1))
203 node->location().warning(message: "Already generated %1 for this project"_L1.arg(args: fileName));
204
205 QString path = outputDir() + QLatin1Char('/') + fileName;
206
207 const auto &outPath = s_redirectDocumentationToDevNull ? QStringLiteral("/dev/null") : path;
208 auto outFile = new QFile(outPath);
209
210 if (!s_redirectDocumentationToDevNull && outFile->exists()) {
211 const QString warningText {"Output file already exists, overwriting %1"_L1.arg(args: outFile->fileName())};
212 if (qEnvironmentVariableIsSet(varName: "QDOC_ALL_OVERWRITES_ARE_WARNINGS"))
213 node->location().warning(message: warningText);
214 else
215 qCDebug(lcQdoc) << qUtf8Printable(warningText);
216 }
217
218 if (!outFile->open(flags: QFile::WriteOnly | QFile::Text)) {
219 node->location().fatal(
220 QStringLiteral("Cannot open output file '%1'").arg(a: outFile->fileName()));
221 }
222
223 qCDebug(lcQdoc, "Writing: %s", qPrintable(path));
224 s_outFileNames << fileName;
225 s_trademarks.clear();
226 return outFile;
227}
228
229/*!
230 Creates the file named \a fileName in the output directory.
231 Attaches a QTextStream to the created file, which is written
232 to all over the place using out().
233 */
234void Generator::beginSubPage(const Node *node, const QString &fileName)
235{
236 Q_ASSERT(node->isPageNode());
237 QFile *outFile = openSubPageFile(node: static_cast<const PageNode*>(node), fileName);
238 auto *out = new QTextStream(outFile);
239 outStreamStack.push(t: out);
240}
241
242/*!
243 Flush the text stream associated with the subpage, and
244 then pop it off the text stream stack and delete it.
245 This terminates output of the subpage.
246 */
247void Generator::endSubPage()
248{
249 outStreamStack.top()->flush();
250 delete outStreamStack.top()->device();
251 delete outStreamStack.pop();
252}
253
254QString Generator::fileBase(const Node *node) const
255{
256 if (!node->isPageNode() && !node->isCollectionNode())
257 node = node->parent();
258
259 if (node->hasFileNameBase())
260 return node->fileNameBase();
261
262 QString base{node->name()};
263 if (base.endsWith(s: ".html"))
264 base.truncate(pos: base.size() - 5);
265
266 if (node->isCollectionNode()) {
267 if (node->isQmlModule())
268 base.append(s: "-qmlmodule");
269 else if (node->isModule())
270 base.append(s: "-module");
271 base.append(s: outputSuffix(node));
272 } else if (node->isTextPageNode()) {
273 if (node->isExample()) {
274 base.prepend(s: "%1-"_L1.arg(args: s_project.toLower()));
275 base.append(s: "-example");
276 }
277 } else if (node->isQmlType()) {
278 /*
279 To avoid file name conflicts in the html directory,
280 we prepend a prefix (by default, "qml-") and an optional suffix
281 to the file name. The suffix, if one exists, is appended to the
282 module name.
283
284 For historical reasons, skip the module name qualifier for QML value types
285 in order to avoid excess redirects in the online docs. TODO: re-assess
286 */
287 if (!node->logicalModuleName().isEmpty() && !node->isQmlBasicType()
288 && (!node->logicalModule()->isInternal() || m_showInternal))
289 base.prepend(s: "%1%2-"_L1.arg(args: node->logicalModuleName(), args: outputSuffix(node)));
290
291 } else if (node->isProxyNode()) {
292 base.append(s: "-%1-proxy"_L1.arg(args: node->tree()->physicalModuleName()));
293 } else {
294 base.clear();
295 const Node *p = node;
296 forever {
297 const Node *pp = p->parent();
298 base.prepend(s: p->name());
299 if (pp == nullptr || pp->name().isEmpty() || pp->isTextPageNode())
300 break;
301 base.prepend(c: '-'_L1);
302 p = pp;
303 }
304 if (node->isNamespace() && !node->name().isEmpty()) {
305 const auto *ns = static_cast<const NamespaceNode *>(node);
306 if (!ns->isDocumentedHere()) {
307 base.append(s: QLatin1String("-sub-"));
308 base.append(s: ns->tree()->camelCaseModuleName());
309 }
310 }
311 base.append(s: outputSuffix(node));
312 }
313
314 base.prepend(s: outputPrefix(node));
315 QString canonicalName{ Utilities::asAsciiPrintable(name: base) };
316 Node *n = const_cast<Node *>(node);
317 n->setFileNameBase(canonicalName);
318 return canonicalName;
319}
320
321/*!
322 Constructs an href link from an example file name, which
323 is a \a path to the example file. If \a fileExt is empty
324 (default value), retrieve the file extension from
325 the generator.
326 */
327QString Generator::linkForExampleFile(const QString &path, const QString &fileExt)
328{
329 QString link{path};
330 link.prepend(s: s_project.toLower() + QLatin1Char('-'));
331
332 QString canonicalName{ Utilities::asAsciiPrintable(name: link) };
333 canonicalName.append(c: QLatin1Char('.'));
334 canonicalName.append(s: fileExt.isEmpty() ? fileExtension() : fileExt);
335 return canonicalName;
336}
337
338/*!
339 Helper function to construct a title for a file or image page
340 included in an example.
341*/
342QString Generator::exampleFileTitle(const ExampleNode *relative, const QString &fileName)
343{
344 QString suffix;
345 if (relative->files().contains(str: fileName))
346 suffix = QLatin1String(" Example File");
347 else if (relative->images().contains(str: fileName))
348 suffix = QLatin1String(" Image File");
349 else
350 return suffix;
351
352 return fileName.mid(position: fileName.lastIndexOf(c: QLatin1Char('/')) + 1) + suffix;
353}
354
355/*!
356 If the \a node has a URL, return the URL as the file name.
357 Otherwise, construct the file name from the fileBase() and
358 either the provided \a extension or fileExtension(), and
359 return the constructed name.
360 */
361QString Generator::fileName(const Node *node, const QString &extension) const
362{
363 if (!node->url().isEmpty())
364 return node->url();
365
366 QString name = fileBase(node) + QLatin1Char('.');
367 return name + (extension.isNull() ? fileExtension() : extension);
368}
369
370/*!
371 Clean the given \a ref to be used as an HTML anchor or an \c xml:id.
372 If \a xmlCompliant is set to \c true, a stricter process is used, as XML
373 is more rigorous in what it accepts. Otherwise, if \a xmlCompliant is set to
374 \c false, the basic HTML transformations are applied.
375
376 More specifically, only XML NCNames are allowed
377 (https://www.w3.org/TR/REC-xml-names/#NT-NCName).
378 */
379QString Generator::cleanRef(const QString &ref, bool xmlCompliant)
380{
381 // XML-compliance is ensured in two ways:
382 // - no digit (0-9) at the beginning of an ID (many IDs do not respect this property)
383 // - no colon (:) anywhere in the ID (occurs very rarely)
384
385 QString clean;
386
387 if (ref.isEmpty())
388 return clean;
389
390 clean.reserve(asize: ref.size() + 20);
391 const QChar c = ref[0];
392 const uint u = c.unicode();
393
394 if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (!xmlCompliant && u >= '0' && u <= '9')) {
395 clean += c;
396 } else if (xmlCompliant && u >= '0' && u <= '9') {
397 clean += QLatin1Char('A') + c;
398 } else if (u == '~') {
399 clean += "dtor.";
400 } else if (u == '_') {
401 clean += "underscore.";
402 } else {
403 clean += QLatin1Char('A');
404 }
405
406 for (int i = 1; i < ref.size(); i++) {
407 const QChar c = ref[i];
408 const uint u = c.unicode();
409 if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9') || u == '-'
410 || u == '_' || (xmlCompliant && u == ':') || u == '.') {
411 clean += c;
412 } else if (c.isSpace()) {
413 clean += QLatin1Char('-');
414 } else if (u == '!') {
415 clean += "-not";
416 } else if (u == '&') {
417 clean += "-and";
418 } else if (u == '<') {
419 clean += "-lt";
420 } else if (u == '=') {
421 clean += "-eq";
422 } else if (u == '>') {
423 clean += "-gt";
424 } else if (u == '#') {
425 clean += QLatin1Char('#');
426 } else {
427 clean += QLatin1Char('-');
428 clean += QString::number(static_cast<int>(u), base: 16);
429 }
430 }
431 return clean;
432}
433
434QMap<QString, QString> &Generator::formattingLeftMap()
435{
436 return s_fmtLeftMaps[format()];
437}
438
439QMap<QString, QString> &Generator::formattingRightMap()
440{
441 return s_fmtRightMaps[format()];
442}
443
444/*!
445 Returns the full document location.
446 */
447QString Generator::fullDocumentLocation(const Node *node)
448{
449 if (node == nullptr)
450 return QString();
451 if (!node->url().isEmpty())
452 return node->url();
453
454 QString parentName;
455 QString anchorRef;
456
457 if (node->isNamespace()) {
458 /*
459 The root namespace has no name - check for this before creating
460 an attribute containing the location of any documentation.
461 */
462 if (!fileBase(node).isEmpty())
463 parentName = fileBase(node) + QLatin1Char('.') + currentGenerator()->fileExtension();
464 else
465 return QString();
466 } else if (node->isQmlType()) {
467 return fileBase(node) + QLatin1Char('.') + currentGenerator()->fileExtension();
468 } else if (node->isTextPageNode() || node->isCollectionNode()) {
469 parentName = fileBase(node) + QLatin1Char('.') + currentGenerator()->fileExtension();
470 } else if (fileBase(node).isEmpty())
471 return QString();
472
473 Node *parentNode = nullptr;
474
475 if ((parentNode = node->parent())) {
476 // use the parent's name unless the parent is the root namespace
477 if (!node->parent()->isNamespace() || !node->parent()->name().isEmpty())
478 parentName = fullDocumentLocation(node: node->parent());
479 }
480
481 switch (node->nodeType()) {
482 case NodeType::Class:
483 case NodeType::Struct:
484 case NodeType::Union:
485 case NodeType::Namespace:
486 case NodeType::Proxy:
487 parentName = fileBase(node) + QLatin1Char('.') + currentGenerator()->fileExtension();
488 break;
489 case NodeType::Function: {
490 const auto *fn = static_cast<const FunctionNode *>(node);
491 switch (fn->metaness()) {
492 case FunctionNode::QmlSignal:
493 anchorRef = QLatin1Char('#') + node->name() + "-signal";
494 break;
495 case FunctionNode::QmlSignalHandler:
496 anchorRef = QLatin1Char('#') + node->name() + "-signal-handler";
497 break;
498 case FunctionNode::QmlMethod:
499 anchorRef = QLatin1Char('#') + node->name() + "-method";
500 break;
501 default:
502 if (fn->isDtor())
503 anchorRef = "#dtor." + fn->name().mid(position: 1);
504 else if (const auto *p = fn->primaryAssociatedProperty(); p && fn->doc().isEmpty())
505 return fullDocumentLocation(node: p);
506 else if (fn->overloadNumber() > 0)
507 anchorRef = QLatin1Char('#') + cleanRef(ref: fn->name()) + QLatin1Char('-')
508 + QString::number(fn->overloadNumber());
509 else
510 anchorRef = QLatin1Char('#') + cleanRef(ref: fn->name());
511 break;
512 }
513 break;
514 }
515 /*
516 Use node->name() instead of fileBase(node) as
517 the latter returns the name in lower-case. For
518 HTML anchors, we need to preserve the case.
519 */
520 case NodeType::Enum:
521 case NodeType::QmlEnum:
522 anchorRef = QLatin1Char('#') + node->name() + "-enum";
523 break;
524 case NodeType::Typedef: {
525 const auto *tdef = static_cast<const TypedefNode *>(node);
526 if (tdef->associatedEnum())
527 return fullDocumentLocation(node: tdef->associatedEnum());
528 } Q_FALLTHROUGH();
529 case NodeType::TypeAlias:
530 anchorRef = QLatin1Char('#') + node->name() + "-typedef";
531 break;
532 case NodeType::Property:
533 anchorRef = QLatin1Char('#') + node->name() + "-prop";
534 break;
535 case NodeType::SharedComment: {
536 if (!node->isPropertyGroup())
537 break;
538 } Q_FALLTHROUGH();
539 case NodeType::QmlProperty:
540 if (node->isAttached())
541 anchorRef = QLatin1Char('#') + node->name() + "-attached-prop";
542 else
543 anchorRef = QLatin1Char('#') + node->name() + "-prop";
544 break;
545 case NodeType::Variable:
546 anchorRef = QLatin1Char('#') + node->name() + "-var";
547 break;
548 case NodeType::QmlType:
549 case NodeType::Page:
550 case NodeType::Group:
551 case NodeType::HeaderFile:
552 case NodeType::Module:
553 case NodeType::QmlModule: {
554 parentName = fileBase(node);
555 parentName.replace(before: QLatin1Char('/'), after: QLatin1Char('-'))
556 .replace(before: QLatin1Char('.'), after: QLatin1Char('-'));
557 parentName += QLatin1Char('.') + currentGenerator()->fileExtension();
558 } break;
559 default:
560 break;
561 }
562
563 if (!node->isClassNode() && !node->isNamespace()) {
564 if (node->isDeprecated())
565 parentName.replace(before: QLatin1Char('.') + currentGenerator()->fileExtension(),
566 after: "-obsolete." + currentGenerator()->fileExtension());
567 }
568
569 return parentName.toLower() + anchorRef;
570}
571
572void Generator::generateAlsoList(const Node *node, CodeMarker *marker)
573{
574 QList<Text> alsoList = node->doc().alsoList();
575 supplementAlsoList(node, alsoList);
576
577 if (!alsoList.isEmpty()) {
578 Text text;
579 text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) << "See also "
580 << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD);
581
582 for (int i = 0; i < alsoList.size(); ++i)
583 text << alsoList.at(i) << Utilities::separator(wordPosition: i, numberOfWords: alsoList.size());
584
585 text << Atom::ParaRight;
586 generateText(text, relative: node, marker);
587 }
588}
589
590const Atom *Generator::generateAtomList(const Atom *atom, const Node *relative, CodeMarker *marker,
591 bool generate, int &numAtoms)
592{
593 while (atom != nullptr) {
594 if (atom->type() == Atom::FormatIf) {
595 int numAtoms0 = numAtoms;
596 bool rightFormat = canHandleFormat(format: atom->string());
597 atom = generateAtomList(atom: atom->next(), relative, marker, generate: generate && rightFormat,
598 numAtoms);
599 if (atom == nullptr)
600 return nullptr;
601
602 if (atom->type() == Atom::FormatElse) {
603 ++numAtoms;
604 atom = generateAtomList(atom: atom->next(), relative, marker, generate: generate && !rightFormat,
605 numAtoms);
606 if (atom == nullptr)
607 return nullptr;
608 }
609
610 if (atom->type() == Atom::FormatEndif) {
611 if (generate && numAtoms0 == numAtoms) {
612 relative->location().warning(QStringLiteral("Output format %1 not handled %2")
613 .arg(args: format(), args: outFileName()));
614 Atom unhandledFormatAtom(Atom::UnhandledFormat, format());
615 generateAtomList(atom: &unhandledFormatAtom, relative, marker, generate, numAtoms);
616 }
617 atom = atom->next();
618 }
619 } else if (atom->type() == Atom::FormatElse || atom->type() == Atom::FormatEndif) {
620 return atom;
621 } else {
622 int n = 1;
623 if (generate) {
624 n += generateAtom(atom, relative, marker);
625 numAtoms += n;
626 }
627 while (n-- > 0)
628 atom = atom->next();
629 }
630 }
631 return nullptr;
632}
633
634/*!
635 Generate the body of the documentation from the qdoc comment
636 found with the entity represented by the \a node.
637 */
638void Generator::generateBody(const Node *node, CodeMarker *marker)
639{
640 const FunctionNode *fn = node->isFunction() ? static_cast<const FunctionNode *>(node) : nullptr;
641 if (!node->hasDoc()) {
642 /*
643 Test for special function, like a destructor or copy constructor,
644 that has no documentation.
645 */
646 if (fn) {
647 if (fn->isDtor()) {
648 Text text;
649 text << "Destroys the instance of ";
650 text << fn->parent()->name() << ".";
651 if (fn->isVirtual())
652 text << " The destructor is virtual.";
653 out() << "<p>";
654 generateText(text, relative: node, marker);
655 out() << "</p>";
656 } else if (fn->isCtor()) {
657 Text text;
658 text << "Default constructs an instance of ";
659 text << fn->parent()->name() << ".";
660 out() << "<p>";
661 generateText(text, relative: node, marker);
662 out() << "</p>";
663 } else if (fn->isCCtor()) {
664 Text text;
665 text << "Copy constructor.";
666 out() << "<p>";
667 generateText(text, relative: node, marker);
668 out() << "</p>";
669 } else if (fn->isMCtor()) {
670 Text text;
671 text << "Move-copy constructor.";
672 out() << "<p>";
673 generateText(text, relative: node, marker);
674 out() << "</p>";
675 } else if (fn->isCAssign()) {
676 Text text;
677 text << "Copy-assignment operator.";
678 out() << "<p>";
679 generateText(text, relative: node, marker);
680 out() << "</p>";
681 } else if (fn->isMAssign()) {
682 Text text;
683 text << "Move-assignment operator.";
684 out() << "<p>";
685 generateText(text, relative: node, marker);
686 out() << "</p>";
687 } else if (!node->isWrapper() && !node->isMarkedReimp()) {
688 if (!fn->isIgnored()) // undocumented functions added by Q_OBJECT
689 node->location().warning(QStringLiteral("No documentation for '%1'")
690 .arg(a: node->plainSignature()));
691 }
692 } else if (!node->isWrapper() && !node->isMarkedReimp()) {
693 // Don't require documentation of things defined in Q_GADGET
694 if (node->name() != QLatin1String("QtGadgetHelper"))
695 node->location().warning(
696 QStringLiteral("No documentation for '%1'").arg(a: node->plainSignature()));
697 }
698 } else if (!node->isSharingComment()) {
699 // Reimplements clause and type alias info precede body text
700 if (fn && !fn->overridesThis().isEmpty())
701 generateReimplementsClause(fn, marker);
702 else if (node->isProperty()) {
703 if (static_cast<const PropertyNode *>(node)->propertyType() != PropertyNode::PropertyType::StandardProperty)
704 generateAddendum(node, type: BindableProperty, marker);
705 }
706
707 if (!generateText(text: node->doc().body(), relative: node, marker)) {
708 if (node->isMarkedReimp())
709 return;
710 }
711
712 if (fn) {
713 if (fn->isQmlSignal())
714 generateAddendum(node, type: QmlSignalHandler, marker);
715 if (fn->isPrivateSignal())
716 generateAddendum(node, type: PrivateSignal, marker);
717 if (fn->isInvokable())
718 generateAddendum(node, type: Invokable, marker);
719 if (fn->hasAssociatedProperties())
720 generateAddendum(node, type: AssociatedProperties, marker);
721 if (fn->hasOverloads() && fn->doc().hasOverloadCommand())
722 generateAddendum(node, type: OverloadNote, marker, prefix: AdmonitionPrefix::None);
723 }
724
725 // Generate warnings
726 if (node->isEnumType()) {
727 const auto *enume = static_cast<const EnumNode *>(node);
728
729 QSet<QString> definedItems;
730 const QList<EnumItem> &items = enume->items();
731 for (const auto &item : items)
732 definedItems.insert(value: item.name());
733
734 const auto &documentedItemList = enume->doc().enumItemNames();
735 QSet<QString> documentedItems(documentedItemList.cbegin(), documentedItemList.cend());
736 const QSet<QString> allItems = definedItems + documentedItems;
737 if (allItems.size() > definedItems.size()
738 || allItems.size() > documentedItems.size()) {
739 for (const auto &it : allItems) {
740 if (!definedItems.contains(value: it)) {
741 QString details;
742 QString best = nearestName(actual: it, candidates: definedItems);
743 if (!best.isEmpty() && !documentedItems.contains(value: best))
744 details = QStringLiteral("Maybe you meant '%1'?").arg(a: best);
745
746 node->doc().location().warning(
747 QStringLiteral("No such enum item '%1' in %2")
748 .arg(args: it, args: node->plainFullName()),
749 details);
750 } else if (!documentedItems.contains(value: it)) {
751 node->doc().location().warning(
752 QStringLiteral("Undocumented enum item '%1' in %2")
753 .arg(args: it, args: node->plainFullName()));
754 }
755 }
756 }
757 } else if (fn) {
758 const QSet<QString> declaredNames = fn->parameters().getNames();
759 const QSet<QString> documentedNames = fn->doc().parameterNames();
760 if (declaredNames != documentedNames) {
761 for (const auto &name : declaredNames) {
762 if (!documentedNames.contains(value: name)) {
763 if (fn->isActive() || fn->isPreliminary()) {
764 // Require no parameter documentation for overrides and overloads,
765 // and only require it for non-overloaded constructors.
766 if (!fn->isMarkedReimp() && !fn->isOverload() &&
767 !(fn->isSomeCtor() && fn->hasOverloads())) {
768 fn->doc().location().warning(
769 QStringLiteral("Undocumented parameter '%1' in %2")
770 .arg(args: name, args: node->plainFullName()));
771 }
772 }
773 }
774 }
775 for (const auto &name : documentedNames) {
776 if (!declaredNames.contains(value: name)) {
777 QString best = nearestName(actual: name, candidates: declaredNames);
778 QString details;
779 if (!best.isEmpty())
780 details = QStringLiteral("Maybe you meant '%1'?").arg(a: best);
781 fn->doc().location().warning(QStringLiteral("No such parameter '%1' in %2")
782 .arg(args: name, args: fn->plainFullName()),
783 details);
784 }
785 }
786 }
787 /*
788 This return value check should be implemented
789 for all functions with a return type.
790 mws 13/12/2018
791 */
792 if (!fn->isDeprecated() && fn->returnsBool() && !fn->isMarkedReimp()
793 && !fn->isOverload()) {
794 if (!fn->doc().body().contains(str: "return"))
795 node->doc().location().warning(
796 QStringLiteral("Undocumented return value "
797 "(hint: use 'return' or 'returns' in the text"));
798 }
799 } else if (node->isQmlProperty()) {
800 if (auto *qpn = static_cast<const QmlPropertyNode *>(node); !qpn->validateDataType())
801 qpn->doc().location().warning(message: "Invalid QML property type: %1"_L1.arg(args: qpn->dataType()));
802 }
803 }
804 generateEnumValuesForQmlReference(node, marker);
805 generateRequiredLinks(node, marker);
806}
807
808/*!
809 Generates either a link to the project folder for example \a node, or a list
810 of links files/images if 'url.examples config' variable is not defined.
811
812 Does nothing for non-example nodes.
813*/
814void Generator::generateRequiredLinks(const Node *node, CodeMarker *marker)
815{
816 if (!node->isExample())
817 return;
818
819 const auto *en = static_cast<const ExampleNode *>(node);
820 QString exampleUrl{Config::instance().get(CONFIG_URL + Config::dot + CONFIG_EXAMPLES).asString()};
821
822 if (exampleUrl.isEmpty()) {
823 if (!en->noAutoList()) {
824 generateFileList(en, marker, images: false); // files
825 generateFileList(en, marker, images: true); // images
826 }
827 } else {
828 generateLinkToExample(en, marker, exampleUrl);
829 }
830}
831
832/*!
833 Generates an external link to the project folder for example \a node.
834 The path to the example replaces a placeholder '\1' character if
835 one is found in the \a baseUrl string. If no such placeholder is found,
836 the path is appended to \a baseUrl, after a '/' character if \a baseUrl did
837 not already end in one.
838*/
839void Generator::generateLinkToExample(const ExampleNode *en, CodeMarker *marker,
840 const QString &baseUrl)
841{
842 QString exampleUrl(baseUrl);
843 QString link;
844#ifndef QT_BOOTSTRAPPED
845 link = QUrl(exampleUrl).host();
846#endif
847 if (!link.isEmpty())
848 link.prepend(s: " @ ");
849 link.prepend(s: "Example project");
850
851 const QLatin1Char separator('/');
852 const QLatin1Char placeholder('\1');
853 if (!exampleUrl.contains(c: placeholder)) {
854 if (!exampleUrl.endsWith(c: separator))
855 exampleUrl += separator;
856 exampleUrl += placeholder;
857 }
858
859 // Construct a path to the example; <install path>/<example name>
860 QString pathRoot;
861 QStringMultiMap *metaTagMap = en->doc().metaTagMap();
862 if (metaTagMap)
863 pathRoot = metaTagMap->value(key: QLatin1String("installpath"));
864 if (pathRoot.isEmpty())
865 pathRoot = Config::instance().get(CONFIG_EXAMPLESINSTALLPATH).asString();
866 QStringList path = QStringList() << pathRoot << en->name();
867 path.removeAll(t: QString());
868
869 Text text;
870 text << Atom::ParaLeft
871 << Atom(Atom::Link, exampleUrl.replace(c: placeholder, after: path.join(sep: separator)))
872 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << Atom(Atom::String, link)
873 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom::ParaRight;
874
875 generateText(text, relative: nullptr, marker);
876}
877
878void Generator::addImageToCopy(const ExampleNode *en, const ResolvedFile& resolved_file)
879{
880 QDir dirInfo;
881 // TODO: [uncentralized-output-directory-structure]
882 const QString prefix("/images/used-in-examples");
883
884 // TODO: Generators probably should not need to keep track of which files were generated.
885 // Understand if we really need this information and where it should
886 // belong, considering that it should be part of whichever system
887 // would actually store the file itself.
888 s_outFileNames << prefix.mid(position: 1) + "/" + resolved_file.get_query();
889
890
891 // TODO: [uncentralized-output-directory-structure]
892 QString imgOutDir = s_outDir + prefix + "/" + QFileInfo{resolved_file.get_query()}.path();
893 if (!dirInfo.mkpath(dirPath: imgOutDir))
894 en->location().fatal(QStringLiteral("Cannot create output directory '%1'").arg(a: imgOutDir));
895 Config::copyFile(location: en->location(), sourceFilePath: resolved_file.get_path(), userFriendlySourceFilePath: QFileInfo{resolved_file.get_query()}.fileName(), targetDirPath: imgOutDir);
896}
897
898// TODO: [multi-purpose-function-with-flag][generate-file-list]
899// Avoid the use of a boolean flag to dispatch to the correct
900// implementation trough branching.
901// We always have to process both images and files, such that we
902// should consider to remove the branching altogheter, performing both
903// operations in a single call.
904// Otherwise, if this turns out to be infeasible, complex or
905// possibly-confusing, consider extracting the processing code outside
906// the function and provide two higer-level dispathing functions for
907// files and images.
908
909/*!
910 This function is called when the documentation for an example is
911 being formatted. It outputs a list of files for the example, which
912 can be the example's source files or the list of images used by the
913 example. The images are copied into a subtree of
914 \c{...doc/html/images/used-in-examples/...}
915*/
916void Generator::generateFileList(const ExampleNode *en, CodeMarker *marker, bool images)
917{
918 Text text;
919 OpenedList openedList(OpenedList::Bullet);
920 QString tag;
921 QStringList paths;
922 Atom::AtomType atomType = Atom::ExampleFileLink;
923
924 if (images) {
925 paths = en->images();
926 tag = "Images:";
927 atomType = Atom::ExampleImageLink;
928 } else { // files
929 paths = en->files();
930 tag = "Files:";
931 }
932 std::sort(first: paths.begin(), last: paths.end(), comp: Generator::comparePaths);
933
934 text << Atom::ParaLeft << tag << Atom::ParaRight;
935 text << Atom(Atom::ListLeft, openedList.styleString());
936
937 for (const auto &path : std::as_const(t&: paths)) {
938 auto maybe_resolved_file{file_resolver.resolve(filename: path)};
939 if (!maybe_resolved_file) {
940 // TODO: [uncentralized-admonition][failed-resolve-file]
941 QString details = std::transform_reduce(
942 first: file_resolver.get_search_directories().cbegin(),
943 last: file_resolver.get_search_directories().cend(),
944 init: u"Searched directories:"_s,
945 binary_op: std::plus(),
946 unary_op: [](const DirectoryPath &directory_path) -> QString { return u' ' + directory_path.value(); }
947 );
948
949 en->location().warning(message: u"(Generator)Cannot find file to quote from: %1"_s.arg(a: path), details);
950
951 continue;
952 }
953
954 const auto &file{*maybe_resolved_file};
955 if (images)
956 addImageToCopy(en, resolved_file: file);
957 else
958 generateExampleFilePage(en, file, marker);
959
960 openedList.next();
961 text << Atom(Atom::ListItemNumber, openedList.numberString())
962 << Atom(Atom::ListItemLeft, openedList.styleString()) << Atom::ParaLeft
963 << Atom(atomType, file.get_query()) << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << file.get_query()
964 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom::ParaRight
965 << Atom(Atom::ListItemRight, openedList.styleString());
966 }
967 text << Atom(Atom::ListRight, openedList.styleString());
968 if (!paths.isEmpty())
969 generateText(text, relative: en, marker);
970}
971
972/*!
973 Recursive writing of HTML files from the root \a node.
974 */
975void Generator::generateDocumentation(Node *node)
976{
977 if (!node->url().isNull())
978 return;
979 if (node->isIndexNode())
980 return;
981 if (node->isInternal() && !m_showInternal)
982 return;
983 if (node->isExternalPage())
984 return;
985
986 /*
987 Obtain a code marker for the source file.
988 */
989 CodeMarker *marker = CodeMarker::markerForFileName(fileName: node->location().filePath());
990
991 if (node->parent() != nullptr) {
992 if (node->isCollectionNode()) {
993 /*
994 A collection node collects: groups, C++ modules, or QML
995 modules. Testing for a CollectionNode must be done
996 before testing for a TextPageNode because a
997 CollectionNode is a PageNode at this point.
998
999 Don't output an HTML page for the collection node unless
1000 the \group, \module, or \qmlmodule command was actually
1001 seen by qdoc in the qdoc comment for the node.
1002
1003 A key prerequisite in this case is the call to
1004 mergeCollections(cn). We must determine whether this
1005 group, module or QML module has members in other
1006 modules. We know at this point that cn's members list
1007 contains only members in the current module. Therefore,
1008 before outputting the page for cn, we must search for
1009 members of cn in the other modules and add them to the
1010 members list.
1011 */
1012 auto *cn = static_cast<CollectionNode *>(node);
1013 if (cn->wasSeen()) {
1014 m_qdb->mergeCollections(c: cn);
1015 beginSubPage(node, fileName: fileName(node));
1016 generateCollectionNode(cn, marker);
1017 endSubPage();
1018 } else if (cn->isGenericCollection()) {
1019 // Currently used only for the module's related orphans page
1020 // but can be generalized for other kinds of collections if
1021 // other use cases pop up.
1022 QString name = cn->name().toLower();
1023 name.replace(c: QChar(' '), after: QString("-"));
1024 QString filename =
1025 cn->tree()->physicalModuleName() + "-" + name + "." + fileExtension();
1026 beginSubPage(node, fileName: filename);
1027 generateGenericCollectionPage(cn, marker);
1028 endSubPage();
1029 }
1030 } else if (node->isTextPageNode()) {
1031 beginSubPage(node, fileName: fileName(node));
1032 generatePageNode(static_cast<PageNode *>(node), marker);
1033 endSubPage();
1034 } else if (node->isAggregate()) {
1035 if ((node->isClassNode() || node->isHeader() || node->isNamespace())
1036 && node->docMustBeGenerated()) {
1037 beginSubPage(node, fileName: fileName(node));
1038 generateCppReferencePage(static_cast<Aggregate *>(node), marker);
1039 endSubPage();
1040 } else if (node->isQmlType()) {
1041 beginSubPage(node, fileName: fileName(node));
1042 auto *qcn = static_cast<QmlTypeNode *>(node);
1043 generateQmlTypePage(qcn, marker);
1044 endSubPage();
1045 } else if (node->isProxyNode()) {
1046 beginSubPage(node, fileName: fileName(node));
1047 generateProxyPage(static_cast<Aggregate *>(node), marker);
1048 endSubPage();
1049 }
1050 }
1051 }
1052
1053 if (node->isAggregate()) {
1054 auto *aggregate = static_cast<Aggregate *>(node);
1055 const NodeList &children = aggregate->childNodes();
1056 for (auto *child : children) {
1057 if (child->isPageNode() && !child->isPrivate()) {
1058 generateDocumentation(node: child);
1059 } else if (!node->parent() && child->isInAPI() && !child->isRelatedNonmember()) {
1060 // Warn if there are documented non-page-generating nodes in the root namespace
1061 child->location().warning(message: u"No documentation generated for %1 '%2' in global scope."_s
1062 .arg(args: typeString(node: child), args: child->name()),
1063 details: u"Maybe you forgot to use the '\\relates' command?"_s);
1064 child->setStatus(Node::DontDocument);
1065 } else if (child->isQmlModule() && !child->wasSeen()) {
1066 // An undocumented QML module that was constructed as a placeholder
1067 auto *qmlModule = static_cast<CollectionNode *>(child);
1068 for (const auto *member : qmlModule->members()) {
1069 member->location().warning(
1070 message: u"Undocumented QML module '%1' referred by type '%2' or its members"_s
1071 .arg(args: qmlModule->name(), args: member->name()),
1072 details: u"Maybe you forgot to document '\\qmlmodule %1'?"_s
1073 .arg(a: qmlModule->name()));
1074 }
1075 } else if (child->isQmlType() && !child->hasDoc()) {
1076 // A placeholder QML type with incorrect module identifier
1077 auto *qmlType = static_cast<QmlTypeNode *>(child);
1078 if (auto qmid = qmlType->logicalModuleName(); !qmid.isEmpty())
1079 qmlType->location().warning(message: u"No such type '%1' in QML module '%2'"_s
1080 .arg(args: qmlType->name(), args&: qmid));
1081 }
1082 }
1083 }
1084}
1085
1086void Generator::generateReimplementsClause(const FunctionNode *fn, CodeMarker *marker)
1087{
1088 if (fn->overridesThis().isEmpty() || !fn->parent()->isClassNode())
1089 return;
1090
1091 auto *cn = static_cast<ClassNode *>(fn->parent());
1092 const FunctionNode *overrides = cn->findOverriddenFunction(fn);
1093 if (overrides && !overrides->isPrivate() && !overrides->parent()->isPrivate()) {
1094 if (overrides->hasDoc()) {
1095 Text text;
1096 text << Atom::ParaLeft << "Reimplements: ";
1097 QString fullName =
1098 overrides->parent()->name()
1099 + "::" + overrides->signature(options: Node::SignaturePlain);
1100 appendFullName(text, apparentNode: overrides->parent(), fullName, actualNode: overrides);
1101 text << "." << Atom::ParaRight;
1102 generateText(text, relative: fn, marker);
1103 } else {
1104 fn->doc().location().warning(
1105 QStringLiteral("Illegal \\reimp; no documented virtual function for %1")
1106 .arg(a: overrides->plainSignature()));
1107 }
1108 return;
1109 }
1110 const PropertyNode *sameName = cn->findOverriddenProperty(fn);
1111 if (sameName && sameName->hasDoc()) {
1112 Text text;
1113 text << Atom::ParaLeft << "Reimplements an access function for property: ";
1114 QString fullName = sameName->parent()->name() + "::" + sameName->name();
1115 appendFullName(text, apparentNode: sameName->parent(), fullName, actualNode: sameName);
1116 text << "." << Atom::ParaRight;
1117 generateText(text, relative: fn, marker);
1118 }
1119}
1120
1121QString Generator::formatSince(const Node *node)
1122{
1123 QStringList since = node->since().split(sep: QLatin1Char(' '));
1124
1125 // If there is only one argument, assume it is the product version number.
1126 if (since.size() == 1) {
1127 const QString productName = Config::instance().get(CONFIG_PRODUCTNAME).asString();
1128 return productName.isEmpty() ? node->since() : productName + " " + since[0];
1129 }
1130
1131 // Otherwise, use the original <project> <version> string.
1132 return node->since();
1133}
1134
1135/*!
1136 \internal
1137 Returns a string representing status information of a \a node.
1138
1139 If a status description is returned, it is one of:
1140 \list
1141 \li Custom status set explicitly in node's documentation using
1142 \c {\meta {status} {<description>}},
1143 \li 'Deprecated [since <version>]' (\\deprecated [<version>]),
1144 \li 'Until <version>',
1145 \li 'Preliminary' (\\preliminary), or
1146 \li The description adopted from associated module's state:
1147 \c {\modulestate {<description>}}.
1148 \endlist
1149
1150 Otherwise, returns \c std::nullopt.
1151*/
1152std::optional<QString> formatStatus(const Node *node, QDocDatabase *qdb)
1153{
1154 QString status;
1155
1156 if (const auto metaMap = node->doc().metaTagMap(); metaMap) {
1157 status = metaMap->value(key: "status");
1158 if (!status.isEmpty())
1159 return {status};
1160 }
1161 const auto &since = node->deprecatedSince();
1162 if (node->status() == Node::Deprecated) {
1163 status = u"Deprecated"_s;
1164 if (!since.isEmpty())
1165 status += " since %1"_L1.arg(args: since);
1166 } else if (!since.isEmpty()) {
1167 status = "Until %1"_L1.arg(args: since);
1168 } else if (node->status() == Node::Preliminary) {
1169 status = u"Preliminary"_s;
1170 } else if (const auto collection = qdb->getModuleNode(relative: node); collection) {
1171 status = collection->state();
1172 }
1173
1174 return status.isEmpty() ? std::nullopt : std::optional(status);
1175}
1176
1177void Generator::generateSince(const Node *node, CodeMarker *marker)
1178{
1179 if (!node->since().isEmpty()) {
1180 Text text;
1181 text << Atom::ParaLeft << "This " << typeString(node) << " was introduced in "
1182 << formatSince(node) << "." << Atom::ParaRight;
1183 generateText(text, relative: node, marker);
1184 }
1185}
1186
1187void Generator::generateNoexceptNote(const Node* node, CodeMarker* marker) {
1188 std::vector<const Node*> nodes;
1189 if (node->isSharedCommentNode()) {
1190 auto shared_node = static_cast<const SharedCommentNode*>(node);
1191 nodes.reserve(n: shared_node->collective().size());
1192 nodes.insert(position: nodes.begin(), first: shared_node->collective().begin(), last: shared_node->collective().end());
1193 } else nodes.push_back(x: node);
1194
1195 std::size_t counter{1};
1196 for (const Node* node : nodes) {
1197 if (node->isFunction(g: Genus::CPP)) {
1198 if (const auto &exception_info = static_cast<const FunctionNode*>(node)->getNoexcept(); exception_info && !(*exception_info).isEmpty()) {
1199 Text text;
1200 text << Atom::NoteLeft
1201 << (nodes.size() > 1 ? QString::fromStdString(s: " ("s + std::to_string(val: counter) + ")"s) : QString::fromStdString(s: "This ") + typeString(node))
1202 << " is noexcept when "
1203 << Atom(Atom::C, marker->markedUpCode(code: *exception_info, nullptr, Location()))
1204 << " is " << Atom(Atom::C, "true") << "."
1205 << Atom::NoteRight;
1206 generateText(text, relative: node, marker);
1207 }
1208 }
1209
1210 ++counter;
1211 }
1212}
1213
1214void Generator::generateStatus(const Node *node, CodeMarker *marker)
1215{
1216 Text text;
1217
1218 switch (node->status()) {
1219 case Node::Active:
1220 // Output the module 'state' description if set.
1221 if (node->isModule() || node->isQmlModule()) {
1222 const QString &state = static_cast<const CollectionNode*>(node)->state();
1223 if (!state.isEmpty()) {
1224 text << Atom::ParaLeft << "This " << typeString(node) << " is in "
1225 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_ITALIC) << state
1226 << Atom(Atom::FormattingRight, ATOM_FORMATTING_ITALIC) << " state."
1227 << Atom::ParaRight;
1228 break;
1229 }
1230 }
1231 if (const auto &version = node->deprecatedSince(); !version.isEmpty()) {
1232 text << Atom::ParaLeft << "This " << typeString(node)
1233 << " is scheduled for deprecation in version "
1234 << version << "." << Atom::ParaRight;
1235 }
1236 break;
1237 case Node::Preliminary:
1238 text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) << "This "
1239 << typeString(node) << " is under development and is subject to change."
1240 << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << Atom::ParaRight;
1241 break;
1242 case Node::Deprecated:
1243 text << Atom::ParaLeft;
1244 if (node->isAggregate())
1245 text << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD);
1246 text << "This " << typeString(node) << " is deprecated";
1247 if (const QString &version = node->deprecatedSince(); !version.isEmpty()) {
1248 text << " since ";
1249 if (node->isQmlNode() && !node->logicalModuleName().isEmpty())
1250 text << node->logicalModuleName() << " ";
1251 text << version;
1252 }
1253
1254 text << ". We strongly advise against using it in new code.";
1255 if (node->isAggregate())
1256 text << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD);
1257 text << Atom::ParaRight;
1258 break;
1259 case Node::Internal:
1260 default:
1261 break;
1262 }
1263 generateText(text, relative: node, marker);
1264}
1265
1266/*!
1267 Generates an addendum note of type \a type for \a node, using \a marker
1268 as the code marker.
1269*/
1270void Generator::generateAddendum(const Node *node, Addendum type, CodeMarker *marker,
1271 AdmonitionPrefix prefix)
1272{
1273 Q_ASSERT(node && !node->name().isEmpty());
1274 Text text;
1275 text << Atom(Atom::DivLeft,
1276 "class=\"admonition %1\""_L1.arg(args: prefix == AdmonitionPrefix::Note ? u"note"_s : u"auto"_s));
1277 text << Atom::ParaLeft;
1278
1279 switch (prefix) {
1280 case AdmonitionPrefix::None:
1281 break;
1282 case AdmonitionPrefix::Note: {
1283 text << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD)
1284 << "Note: " << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD);
1285 break;
1286 }
1287 }
1288
1289 switch (type) {
1290 case Invokable:
1291 text << "This function can be invoked via the meta-object system and from QML. See "
1292 << Atom(Atom::AutoLink, "Q_INVOKABLE")
1293 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
1294 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << ".";
1295 break;
1296 case PrivateSignal:
1297 text << "This is a private signal. It can be used in signal connections "
1298 "but cannot be emitted by the user.";
1299 break;
1300 case QmlSignalHandler:
1301 {
1302 QString handler(node->name());
1303 qsizetype prefixLocation = handler.lastIndexOf(ch: '.', from: -2) + 1;
1304 handler[prefixLocation] = handler[prefixLocation].toTitleCase();
1305 handler.insert(i: prefixLocation, s: QLatin1String("on"));
1306 text << "The corresponding handler is "
1307 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_TELETYPE) << handler
1308 << Atom(Atom::FormattingRight, ATOM_FORMATTING_TELETYPE) << ".";
1309 break;
1310 }
1311 case AssociatedProperties:
1312 {
1313 if (!node->isFunction())
1314 return;
1315 const auto *fn = static_cast<const FunctionNode *>(node);
1316 auto nodes = fn->associatedProperties();
1317 if (nodes.isEmpty())
1318 return;
1319 std::sort(first: nodes.begin(), last: nodes.end(), comp: Node::nodeNameLessThan);
1320
1321 // Group properties by their role for more concise output
1322 QMap<PropertyNode::FunctionRole, QList<const PropertyNode *>> roleGroups;
1323 for (const auto *n : std::as_const(t&: nodes)) {
1324 const auto *pn = static_cast<const PropertyNode *>(n);
1325 PropertyNode::FunctionRole role = pn->role(functionNode: fn);
1326 roleGroups[role].append(t: pn);
1327 }
1328
1329 // Generate text for each role group in an explicit order
1330 static constexpr PropertyNode::FunctionRole roleOrder[] = {
1331 PropertyNode::FunctionRole::Getter,
1332 PropertyNode::FunctionRole::Setter,
1333 PropertyNode::FunctionRole::Resetter,
1334 PropertyNode::FunctionRole::Notifier,
1335 PropertyNode::FunctionRole::Bindable,
1336 };
1337 for (auto role : roleOrder) {
1338 const auto it = roleGroups.constFind(key: role);
1339 if (it == roleGroups.cend())
1340 continue;
1341
1342 const auto &properties = it.value();
1343
1344 QString msg;
1345 switch (role) {
1346 case PropertyNode::FunctionRole::Getter:
1347 msg = u"Getter function"_s;
1348 break;
1349 case PropertyNode::FunctionRole::Setter:
1350 msg = u"Setter function"_s;
1351 break;
1352 case PropertyNode::FunctionRole::Resetter:
1353 msg = u"Resetter function"_s;
1354 break;
1355 case PropertyNode::FunctionRole::Notifier:
1356 msg = u"Notifier signal"_s;
1357 break;
1358 case PropertyNode::FunctionRole::Bindable:
1359 msg = u"Bindable function"_s;
1360 break;
1361 default:
1362 continue;
1363 }
1364
1365 if (properties.size() == 1) {
1366 const auto *pn = properties.first();
1367 text << msg << u" for property "_s << Atom(Atom::Link, pn->name())
1368 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << pn->name()
1369 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << u". "_s;
1370 } else {
1371 text << msg << u" for properties "_s;
1372 for (qsizetype i = 0; i < properties.size(); ++i) {
1373 const auto *pn = properties.at(i);
1374 text << Atom(Atom::Link, pn->name())
1375 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << pn->name()
1376 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK)
1377 << Utilities::separator(wordPosition: i, numberOfWords: properties.size());
1378 }
1379 text << u" "_s;
1380 }
1381 }
1382 break;
1383 }
1384 case BindableProperty:
1385 {
1386 text << "This property supports "
1387 << Atom(Atom::Link, "QProperty")
1388 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << "QProperty"
1389 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
1390 text << " bindings.";
1391 break;
1392 }
1393 case OverloadNote:
1394 {
1395 const auto *func = static_cast<const FunctionNode *>(node);
1396
1397 if (func->isSignal() || func->isSlot()) {
1398 QString functionType = func->isSignal() ? "signal" : "slot";
1399 const QString &configKey = func->isSignal() ? "overloadedsignalstarget" : "overloadedslotstarget";
1400 const QString &defaultTarget = func->isSignal() ? "connecting-overloaded-signals" : "connecting-overloaded-slots";
1401 const QString &linkTarget = Config::instance().get(var: configKey).asString(defaultString: defaultTarget);
1402
1403 text << "This " << functionType << " is overloaded. ";
1404
1405 QString snippet = generateOverloadSnippet(func);
1406 if (!snippet.isEmpty()) {
1407 text << "To connect to this " << functionType << ":\n\n"
1408 << Atom(Atom::Code, snippet) << "\n";
1409 }
1410
1411 text << "For more examples and approaches, see "
1412 << Atom(Atom::Link, linkTarget)
1413 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
1414 << "connecting to overloaded " << functionType << "s"
1415 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << ".";
1416 } else {
1417 const auto &args = node->doc().overloadList();
1418 if (args.first().first.isEmpty()) {
1419 text << "This is an overloaded function.";
1420 } else {
1421 QString target = args.first().first;
1422 // If the target is not fully qualified and we have a parent class context,
1423 // attempt to qualify it to improve link resolution
1424 if (!target.contains(s: "::")) {
1425 const auto *parent = node->parent();
1426 if (parent && (parent->isClassNode() || parent->isNamespace())) {
1427 target = parent->name() + "::" + target;
1428 }
1429 }
1430 text << "This function overloads " << Atom(Atom::AutoLink, target) << ".";
1431 }
1432 }
1433 break;
1434 }
1435 default:
1436 return;
1437 }
1438
1439 text << Atom::ParaRight
1440 << Atom::DivRight;
1441 generateText(text, relative: node, marker);
1442}
1443
1444/*!
1445 Generate the documentation for \a relative. i.e. \a relative
1446 is the node that represents the entity where a qdoc comment
1447 was found, and \a text represents the qdoc comment.
1448 */
1449bool Generator::generateText(const Text &text, const Node *relative, CodeMarker *marker)
1450{
1451 bool result = false;
1452 if (text.firstAtom() != nullptr) {
1453 int numAtoms = 0;
1454 initializeTextOutput();
1455 generateAtomList(atom: text.firstAtom(), relative, marker, generate: true, numAtoms);
1456 result = true;
1457 }
1458 return result;
1459}
1460
1461/*
1462 The node is an aggregate, typically a class node, which has
1463 a threadsafeness level. This function checks all the children
1464 of the node to see if they are exceptions to the node's
1465 threadsafeness. If there are any exceptions, the exceptions
1466 are added to the appropriate set (reentrant, threadsafe, and
1467 nonreentrant, and true is returned. If there are no exceptions,
1468 the three node lists remain empty and false is returned.
1469 */
1470bool Generator::hasExceptions(const Node *node, NodeList &reentrant, NodeList &threadsafe,
1471 NodeList &nonreentrant)
1472{
1473 bool result = false;
1474 Node::ThreadSafeness ts = node->threadSafeness();
1475 const NodeList &children = static_cast<const Aggregate *>(node)->childNodes();
1476 for (auto child : children) {
1477 if (!child->isDeprecated()) {
1478 switch (child->threadSafeness()) {
1479 case Node::Reentrant:
1480 reentrant.append(t: child);
1481 if (ts == Node::ThreadSafe)
1482 result = true;
1483 break;
1484 case Node::ThreadSafe:
1485 threadsafe.append(t: child);
1486 if (ts == Node::Reentrant)
1487 result = true;
1488 break;
1489 case Node::NonReentrant:
1490 nonreentrant.append(t: child);
1491 result = true;
1492 break;
1493 default:
1494 break;
1495 }
1496 }
1497 }
1498 return result;
1499}
1500
1501/*!
1502 Returns \c true if a trademark symbol should be appended to the
1503 output as determined by \a atom. Trademarks are tracked via the
1504 use of the \\tm formatting command.
1505
1506 Returns true if:
1507
1508 \list
1509 \li \a atom is of type Atom::FormattingRight containing
1510 ATOM_FORMATTING_TRADEMARK, and
1511 \li The trademarked string is the first appearance on the
1512 current sub-page.
1513 \endlist
1514*/
1515bool Generator::appendTrademark(const Atom *atom)
1516{
1517 if (atom->type() != Atom::FormattingRight)
1518 return false;
1519 if (atom->string() != ATOM_FORMATTING_TRADEMARK)
1520 return false;
1521
1522 if (atom->count() > 1) {
1523 if (s_trademarks.contains(value: atom->string(i: 1)))
1524 return false;
1525 s_trademarks << atom->string(i: 1);
1526 }
1527
1528 return true;
1529}
1530
1531static void startNote(Text &text)
1532{
1533 text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD)
1534 << "Note:" << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << " ";
1535}
1536
1537/*!
1538 Generates text that explains how threadsafe and/or reentrant
1539 \a node is.
1540 */
1541void Generator::generateThreadSafeness(const Node *node, CodeMarker *marker)
1542{
1543 Text text, rlink, tlink;
1544 NodeList reentrant;
1545 NodeList threadsafe;
1546 NodeList nonreentrant;
1547 Node::ThreadSafeness ts = node->threadSafeness();
1548 bool exceptions = false;
1549
1550 rlink << Atom(Atom::Link, "reentrant") << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
1551 << "reentrant" << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
1552
1553 tlink << Atom(Atom::Link, "thread-safe") << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
1554 << "thread-safe" << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
1555
1556 switch (ts) {
1557 case Node::UnspecifiedSafeness:
1558 break;
1559 case Node::NonReentrant:
1560 text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD)
1561 << "Warning:" << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << " This "
1562 << typeString(node) << " is not " << rlink << "." << Atom::ParaRight;
1563 break;
1564 case Node::Reentrant:
1565 case Node::ThreadSafe:
1566 startNote(text);
1567 if (node->isAggregate()) {
1568 exceptions = hasExceptions(node, reentrant, threadsafe, nonreentrant);
1569 text << "All functions in this " << typeString(node) << " are ";
1570 if (ts == Node::ThreadSafe)
1571 text << tlink;
1572 else
1573 text << rlink;
1574
1575 if (!exceptions || (ts == Node::Reentrant && !threadsafe.isEmpty()))
1576 text << ".";
1577 else
1578 text << " with the following exceptions:";
1579 } else {
1580 text << "This " << typeString(node) << " is ";
1581 if (ts == Node::ThreadSafe)
1582 text << tlink;
1583 else
1584 text << rlink;
1585 text << ".";
1586 }
1587 text << Atom::ParaRight;
1588 break;
1589 default:
1590 break;
1591 }
1592 generateText(text, relative: node, marker);
1593
1594 if (exceptions) {
1595 text.clear();
1596 if (ts == Node::Reentrant) {
1597 if (!nonreentrant.isEmpty()) {
1598 startNote(text);
1599 text << "These functions are not " << rlink << ":" << Atom::ParaRight;
1600 signatureList(nodes: nonreentrant, relative: node, marker);
1601 }
1602 if (!threadsafe.isEmpty()) {
1603 text.clear();
1604 startNote(text);
1605 text << "These functions are also " << tlink << ":" << Atom::ParaRight;
1606 generateText(text, relative: node, marker);
1607 signatureList(nodes: threadsafe, relative: node, marker);
1608 }
1609 } else { // thread-safe
1610 if (!reentrant.isEmpty()) {
1611 startNote(text);
1612 text << "These functions are only " << rlink << ":" << Atom::ParaRight;
1613 signatureList(nodes: reentrant, relative: node, marker);
1614 }
1615 if (!nonreentrant.isEmpty()) {
1616 text.clear();
1617 startNote(text);
1618 text << "These functions are not " << rlink << ":" << Atom::ParaRight;
1619 signatureList(nodes: nonreentrant, relative: node, marker);
1620 }
1621 }
1622 }
1623}
1624
1625/*!
1626 \internal
1627
1628 Generates text that describes the comparison category of \a node.
1629 The CodeMarker \a marker is passed along to generateText().
1630 */
1631bool Generator::generateComparisonCategory(const Node *node, CodeMarker *marker)
1632{
1633 auto category{node->comparisonCategory()};
1634 if (category == ComparisonCategory::None)
1635 return false;
1636
1637 Text text;
1638 text << Atom::ParaLeft << "This %1 is "_L1.arg(args: typeString(node))
1639 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_ITALIC)
1640 << QString::fromStdString(s: comparisonCategoryAsString(category))
1641 << ((category == ComparisonCategory::Equality) ? "-"_L1 : "ly "_L1)
1642 << Atom(Atom::String, "comparable"_L1)
1643 << Atom(Atom::FormattingRight, ATOM_FORMATTING_ITALIC)
1644 << "."_L1 << Atom::ParaRight;
1645 generateText(text, relative: node, marker);
1646 return true;
1647}
1648
1649/*!
1650 Generates a list of types that compare to \a node with the comparison
1651 category that applies for the relationship, followed by (an optional)
1652 descriptive text.
1653
1654 Returns \c true if text was generated, \c false otherwise.
1655 */
1656bool Generator::generateComparisonList(const Node *node)
1657{
1658 Q_ASSERT(node);
1659 if (!node->doc().comparesWithMap())
1660 return false;
1661
1662 Text relationshipText;
1663 for (auto [key, description] : node->doc().comparesWithMap()->asKeyValueRange()) {
1664 const QString &category = QString::fromStdString(s: comparisonCategoryAsString(category: key));
1665
1666 relationshipText << Atom::ParaLeft << "This %1 is "_L1.arg(args: typeString(node))
1667 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) << category
1668 << ((key == ComparisonCategory::Equality) ? "-"_L1 : "ly "_L1)
1669 << "comparable"_L1
1670 << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD)
1671 << " with "_L1;
1672
1673 const QStringList types{description.firstAtom()->string().split(sep: ';'_L1)};
1674 for (const auto &name : types)
1675 relationshipText << Atom(Atom::AutoLink, name)
1676 << Utilities::separator(wordPosition: types.indexOf(str: name), numberOfWords: types.size());
1677
1678 relationshipText << Atom(Atom::ParaRight) << description;
1679 }
1680
1681 generateText(text: relationshipText, relative: node, marker: nullptr);
1682 return !relationshipText.isEmpty();
1683}
1684
1685/*!
1686 Traverses the database recursively to generate all the documentation.
1687 */
1688void Generator::generateDocs()
1689{
1690 s_currentGenerator = this;
1691 generateDocumentation(node: m_qdb->primaryTreeRoot());
1692}
1693
1694Generator *Generator::generatorForFormat(const QString &format)
1695{
1696 for (const auto &generator : std::as_const(t&: s_generators)) {
1697 if (generator->format() == format)
1698 return generator;
1699 }
1700 return nullptr;
1701}
1702
1703QString Generator::indent(int level, const QString &markedCode)
1704{
1705 if (level == 0)
1706 return markedCode;
1707
1708 QString t;
1709 int column = 0;
1710
1711 int i = 0;
1712 while (i < markedCode.size()) {
1713 if (markedCode.at(i) == QLatin1Char('\n')) {
1714 column = 0;
1715 } else {
1716 if (column == 0) {
1717 for (int j = 0; j < level; j++)
1718 t += QLatin1Char(' ');
1719 }
1720 column++;
1721 }
1722 t += markedCode.at(i: i++);
1723 }
1724 return t;
1725}
1726
1727void Generator::initialize()
1728{
1729 Config &config = Config::instance();
1730 s_outputFormats = config.getOutputFormats();
1731 s_redirectDocumentationToDevNull = config.get(CONFIG_REDIRECTDOCUMENTATIONTODEVNULL).asBool();
1732
1733 for (auto &g : s_generators) {
1734 if (s_outputFormats.contains(value: g->format())) {
1735 s_currentGenerator = g;
1736 g->initializeGenerator();
1737 }
1738 }
1739
1740 const auto &configFormatting = config.subVars(CONFIG_FORMATTING);
1741 for (const auto &n : configFormatting) {
1742 QString formattingDotName = CONFIG_FORMATTING + Config::dot + n;
1743 const auto &formattingDotNames = config.subVars(var: formattingDotName);
1744 for (const auto &f : formattingDotNames) {
1745 const auto &configVar = config.get(var: formattingDotName + Config::dot + f);
1746 QString def{configVar.asString()};
1747 if (!def.isEmpty()) {
1748 int numParams = Config::numParams(value: def);
1749 int numOccs = def.count(s: "\1");
1750 if (numParams != 1) {
1751 configVar.location().warning(QStringLiteral("Formatting '%1' must "
1752 "have exactly one "
1753 "parameter (found %2)")
1754 .arg(a: n, fieldWidth: numParams));
1755 } else if (numOccs > 1) {
1756 configVar.location().fatal(QStringLiteral("Formatting '%1' must "
1757 "contain exactly one "
1758 "occurrence of '\\1' "
1759 "(found %2)")
1760 .arg(a: n, fieldWidth: numOccs));
1761 } else {
1762 int paramPos = def.indexOf(s: "\1");
1763 s_fmtLeftMaps[f].insert(key: n, value: def.left(n: paramPos));
1764 s_fmtRightMaps[f].insert(key: n, value: def.mid(position: paramPos + 1));
1765 }
1766 }
1767 }
1768 }
1769
1770 s_project = config.get(CONFIG_PROJECT).asString();
1771 s_outDir = config.getOutputDir();
1772 s_outSubdir = s_outDir.mid(position: s_outDir.lastIndexOf(c: '/') + 1);
1773
1774 s_outputPrefixes.clear();
1775 QStringList items{config.get(CONFIG_OUTPUTPREFIXES).asStringList()};
1776 if (!items.isEmpty()) {
1777 for (const auto &prefix : items)
1778 s_outputPrefixes[prefix] =
1779 config.get(CONFIG_OUTPUTPREFIXES + Config::dot + prefix).asString();
1780 }
1781 if (!items.contains(str: u"QML"_s))
1782 s_outputPrefixes[u"QML"_s] = u"qml-"_s;
1783
1784 s_outputSuffixes.clear();
1785 for (const auto &suffix : config.get(CONFIG_OUTPUTSUFFIXES).asStringList())
1786 s_outputSuffixes[suffix] = config.get(CONFIG_OUTPUTSUFFIXES
1787 + Config::dot + suffix).asString();
1788
1789 s_noLinkErrors = config.get(CONFIG_NOLINKERRORS).asBool();
1790 s_autolinkErrors = config.get(CONFIG_AUTOLINKERRORS).asBool();
1791}
1792
1793/*!
1794 Creates template-specific subdirs (e.g. /styles and /scripts for HTML)
1795 and copies the files to them.
1796 */
1797void Generator::copyTemplateFiles(const QString &configVar, const QString &subDir)
1798{
1799 // TODO: [resolving-files-unlinked-to-doc]
1800 // This is another case of resolving files, albeit it doesn't use Doc::resolveFile.
1801 // While it may be left out of a first iteration of the file
1802 // resolution logic, it should later be integrated into it.
1803 // This should come naturally when the output directory logic is
1804 // extracted and copying a file should require a validated
1805 // intermediate format.
1806 // Do note that what is done here is a bit different from the
1807 // resolve file routine that is done for other user-given paths.
1808 // Thas is, the paths will always be absolute and not relative as
1809 // they are resolved from the configuration.
1810 // Ideally, this could be solved in the configuration already,
1811 // together with the other configuration resolution processes that
1812 // do not abide by the same constraints that, for example, snippet
1813 // resolution uses.
1814 Config &config = Config::instance();
1815 QStringList files = config.getCanonicalPathList(var: configVar, flags: Config::Validate);
1816 const auto &loc = config.get(var: configVar).location();
1817 if (!files.isEmpty()) {
1818 QDir dirInfo;
1819 // TODO: [uncentralized-output-directory-structure]
1820 // As with other places in the generation pass, the details of
1821 // where something is saved in the output directory are spread
1822 // to whichever part of the generation does the saving.
1823 // It is hence complex to build a model of how an output
1824 // directory looks like, as the knowledge has no specific
1825 // entry point or chain-path that can be followed in full.
1826 // Each of those operations should be centralized in a system
1827 // that uniquely knows what the format of the output-directory
1828 // is and how to perform operations on it.
1829 // Later, move this operation to that centralized system.
1830 QString templateDir = s_outDir + QLatin1Char('/') + subDir;
1831 if (!dirInfo.exists(name: templateDir) && !dirInfo.mkdir(dirName: templateDir)) {
1832 // TODO: [uncentralized-admonition]
1833 loc.fatal(QStringLiteral("Cannot create %1 directory '%2'").arg(args: subDir, args&: templateDir));
1834 } else {
1835 for (const auto &file : files) {
1836 if (!file.isEmpty())
1837 Config::copyFile(location: loc, sourceFilePath: file, userFriendlySourceFilePath: file, targetDirPath: templateDir);
1838 }
1839 }
1840 }
1841}
1842
1843/*!
1844 Reads format-specific variables from config, sets output
1845 (sub)directories, creates them on the filesystem and copies the
1846 template-specific files.
1847 */
1848void Generator::initializeFormat()
1849{
1850 Config &config = Config::instance();
1851 s_outFileNames.clear();
1852 s_useOutputSubdirs = true;
1853 if (config.get(var: format() + Config::dot + "nosubdirs").asBool())
1854 resetUseOutputSubdirs();
1855
1856 if (s_outputFormats.isEmpty())
1857 return;
1858
1859 s_outDir = config.getOutputDir(format: format());
1860 if (s_outDir.isEmpty()) {
1861 Location().fatal(QStringLiteral("No output directory specified in "
1862 "configuration file or on the command line"));
1863 } else {
1864 s_outSubdir = s_outDir.mid(position: s_outDir.lastIndexOf(c: '/') + 1);
1865 }
1866
1867 QDir outputDir(s_outDir);
1868 if (outputDir.exists()) {
1869 if (!config.generating() && Generator::useOutputSubdirs()) {
1870 if (!outputDir.isEmpty())
1871 Location().error(QStringLiteral("Output directory '%1' exists but is not empty")
1872 .arg(a: s_outDir));
1873 }
1874 } else if (!outputDir.mkpath(QStringLiteral("."))) {
1875 Location().fatal(QStringLiteral("Cannot create output directory '%1'").arg(a: s_outDir));
1876 }
1877
1878 // Output directory exists, which is enough for prepare phase.
1879 if (config.preparing())
1880 return;
1881
1882 const QLatin1String imagesDir("images");
1883 if (!outputDir.exists(name: imagesDir) && !outputDir.mkdir(dirName: imagesDir))
1884 Location().fatal(QStringLiteral("Cannot create images directory '%1'").arg(a: outputDir.filePath(fileName: imagesDir)));
1885
1886 copyTemplateFiles(configVar: format() + Config::dot + CONFIG_STYLESHEETS, subDir: "style");
1887 copyTemplateFiles(configVar: format() + Config::dot + CONFIG_SCRIPTS, subDir: "scripts");
1888 copyTemplateFiles(configVar: format() + Config::dot + CONFIG_EXTRAIMAGES, subDir: "images");
1889
1890 // Use a format-specific .quotinginformation if defined, otherwise a global value
1891 if (config.subVars(var: format()).contains(CONFIG_QUOTINGINFORMATION))
1892 m_quoting = config.get(var: format() + Config::dot + CONFIG_QUOTINGINFORMATION).asBool();
1893 else
1894 m_quoting = config.get(CONFIG_QUOTINGINFORMATION).asBool();
1895}
1896
1897/*!
1898 Updates the generator's m_showInternal from the Config.
1899 */
1900void Generator::initializeGenerator()
1901{
1902 m_showInternal = Config::instance().showInternal();
1903}
1904
1905bool Generator::matchAhead(const Atom *atom, Atom::AtomType expectedAtomType)
1906{
1907 return atom->next() && atom->next()->type() == expectedAtomType;
1908}
1909
1910/*!
1911 Used for writing to the current output stream. Returns a
1912 reference to the current output stream, which is then used
1913 with the \c {<<} operator for writing.
1914 */
1915QTextStream &Generator::out()
1916{
1917 return *outStreamStack.top();
1918}
1919
1920QString Generator::outFileName()
1921{
1922 return QFileInfo(static_cast<QFile *>(out().device())->fileName()).fileName();
1923}
1924
1925QString Generator::outputPrefix(const Node *node)
1926{
1927 // Omit prefix for module pages
1928 if (node->isPageNode() && !node->isCollectionNode()) {
1929 switch (node->genus()) {
1930 case Genus::QML:
1931 return s_outputPrefixes[u"QML"_s];
1932 case Genus::CPP:
1933 return s_outputPrefixes[u"CPP"_s];
1934 default:
1935 break;
1936 }
1937 }
1938 return QString();
1939}
1940
1941QString Generator::outputSuffix(const Node *node)
1942{
1943 if (node->isPageNode()) {
1944 switch (node->genus()) {
1945 case Genus::QML:
1946 return s_outputSuffixes[u"QML"_s];
1947 case Genus::CPP:
1948 return s_outputSuffixes[u"CPP"_s];
1949 default:
1950 break;
1951 }
1952 }
1953
1954 return QString();
1955}
1956
1957bool Generator::parseArg(const QString &src, const QString &tag, int *pos, int n,
1958 QStringView *contents, QStringView *par1)
1959{
1960#define SKIP_CHAR(c) \
1961 if (i >= n || src[i] != c) \
1962 return false; \
1963 ++i;
1964
1965#define SKIP_SPACE \
1966 while (i < n && src[i] == ' ') \
1967 ++i;
1968
1969 qsizetype i = *pos;
1970 qsizetype j {};
1971
1972 // assume "<@" has been parsed outside
1973 // SKIP_CHAR('<');
1974 // SKIP_CHAR('@');
1975
1976 if (tag != QStringView(src).mid(pos: i, n: tag.size())) {
1977 return false;
1978 }
1979
1980 // skip tag
1981 i += tag.size();
1982
1983 // parse stuff like: linkTag("(<@link node=\"([^\"]+)\">).*(</@link>)");
1984 if (par1) {
1985 SKIP_SPACE;
1986 // read parameter name
1987 j = i;
1988 while (i < n && src[i].isLetter())
1989 ++i;
1990 if (src[i] == '=') {
1991 SKIP_CHAR('=');
1992 SKIP_CHAR('"');
1993 // skip parameter name
1994 j = i;
1995 while (i < n && src[i] != '"')
1996 ++i;
1997 *par1 = QStringView(src).mid(pos: j, n: i - j);
1998 SKIP_CHAR('"');
1999 SKIP_SPACE;
2000 }
2001 }
2002 SKIP_SPACE;
2003 SKIP_CHAR('>');
2004
2005 // find contents up to closing "</@tag>
2006 j = i;
2007 for (; true; ++i) {
2008 if (i + 4 + tag.size() > n)
2009 return false;
2010 if (src[i] != '<')
2011 continue;
2012 if (src[i + 1] != '/')
2013 continue;
2014 if (src[i + 2] != '@')
2015 continue;
2016 if (tag != QStringView(src).mid(pos: i + 3, n: tag.size()))
2017 continue;
2018 if (src[i + 3 + tag.size()] != '>')
2019 continue;
2020 break;
2021 }
2022
2023 *contents = QStringView(src).mid(pos: j, n: i - j);
2024
2025 i += tag.size() + 4;
2026
2027 *pos = i;
2028 return true;
2029#undef SKIP_CHAR
2030#undef SKIP_SPACE
2031}
2032
2033QString Generator::plainCode(const QString &markedCode)
2034{
2035 QString t = markedCode;
2036 t.replace(re: tag, after: QString());
2037 t.replace(before: quot, after: QLatin1String("\""));
2038 t.replace(before: gt, after: QLatin1String(">"));
2039 t.replace(before: lt, after: QLatin1String("<"));
2040 t.replace(before: amp, after: QLatin1String("&"));
2041 return t;
2042}
2043
2044int Generator::skipAtoms(const Atom *atom, Atom::AtomType type) const
2045{
2046 int skipAhead = 0;
2047 atom = atom->next();
2048 while (atom && atom->type() != type) {
2049 skipAhead++;
2050 atom = atom->next();
2051 }
2052 return skipAhead;
2053}
2054
2055/*!
2056 Resets the variables used during text output.
2057 */
2058void Generator::initializeTextOutput()
2059{
2060 m_inLink = false;
2061 m_inContents = false;
2062 m_inSectionHeading = false;
2063 m_inTableHeader = false;
2064 m_numTableRows = 0;
2065 m_threeColumnEnumValueTable = true;
2066 m_link.clear();
2067 m_sectionNumber.clear();
2068}
2069
2070void Generator::supplementAlsoList(const Node *node, QList<Text> &alsoList)
2071{
2072 if (node->isFunction() && !node->isMacro()) {
2073 const auto fn = static_cast<const FunctionNode *>(node);
2074 if (fn->overloadNumber() == 0) {
2075 QString alternateName;
2076 const FunctionNode *alternateFunc = nullptr;
2077
2078 if (fn->name().startsWith(s: "set") && fn->name().size() >= 4) {
2079 alternateName = fn->name()[3].toLower();
2080 alternateName += fn->name().mid(position: 4);
2081 alternateFunc = fn->parent()->findFunctionChild(name: alternateName, parameters: QString());
2082
2083 if (!alternateFunc) {
2084 alternateName = "is" + fn->name().mid(position: 3);
2085 alternateFunc = fn->parent()->findFunctionChild(name: alternateName, parameters: QString());
2086 if (!alternateFunc) {
2087 alternateName = "has" + fn->name().mid(position: 3);
2088 alternateFunc = fn->parent()->findFunctionChild(name: alternateName, parameters: QString());
2089 }
2090 }
2091 } else if (!fn->name().isEmpty()) {
2092 alternateName = "set";
2093 alternateName += fn->name()[0].toUpper();
2094 alternateName += fn->name().mid(position: 1);
2095 alternateFunc = fn->parent()->findFunctionChild(name: alternateName, parameters: QString());
2096 }
2097
2098 if (alternateFunc && alternateFunc->access() != Access::Private) {
2099 int i;
2100 for (i = 0; i < alsoList.size(); ++i) {
2101 if (alsoList.at(i).toString().contains(s: alternateName))
2102 break;
2103 }
2104
2105 if (i == alsoList.size()) {
2106 if (alternateFunc->isDeprecated() && !fn->isDeprecated())
2107 return;
2108 alternateName += "()";
2109
2110 Text also;
2111 also << Atom(Atom::Link, alternateName)
2112 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << alternateName
2113 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
2114 alsoList.prepend(t: also);
2115 }
2116 }
2117 }
2118 }
2119}
2120
2121void Generator::generateEnumValuesForQmlReference(const Node *node, CodeMarker *marker)
2122{
2123 const NativeEnum *nativeEnum{nullptr};
2124 if (auto *ne_if = dynamic_cast<const NativeEnumInterface *>(node))
2125 nativeEnum = ne_if->nativeEnum();
2126 else
2127 return;
2128
2129 if (!nativeEnum->enumNode())
2130 return;
2131
2132 // Retrieve atoms from C++ enum \value list
2133 const auto body{nativeEnum->enumNode()->doc().body()};
2134 const auto *start{body.firstAtom()};
2135 Text text;
2136
2137 while ((start = start->find(t: Atom::ListLeft, ATOM_LIST_VALUE))) {
2138 const auto end = start->find(t: Atom::ListRight, ATOM_LIST_VALUE);
2139 // Skip subsequent ListLeft atoms, collating multiple lists into one
2140 text << body.subText(begin: text.isEmpty() ? start : start->next(), end);
2141 start = end;
2142 }
2143 if (text.isEmpty())
2144 return;
2145
2146 text << Atom(Atom::ListRight, ATOM_LIST_VALUE);
2147 if (marker)
2148 generateText(text, relative: node, marker);
2149 else
2150 generateText(text, relative: node);
2151}
2152
2153void Generator::terminate()
2154{
2155 for (const auto &generator : std::as_const(t&: s_generators)) {
2156 if (s_outputFormats.contains(value: generator->format()))
2157 generator->terminateGenerator();
2158 }
2159
2160 // REMARK: Generators currently, due to recent changes and the
2161 // transitive nature of the current codebase, receive some of
2162 // their dependencies in the constructor and some of them in their
2163 // initialize-terminate lifetime.
2164 // This means that generators need to be constructed and
2165 // destructed between usages such that if multiple usages are
2166 // required, the generators present in the list will have been
2167 // destroyed by then such that accessing them would be an error.
2168 // The current codebase calls initialize and the correspective
2169 // terminate with the same scope as the lifetime of the
2170 // generators.
2171 // Then, clearing the list ensures that, if another generator
2172 // execution is needed, the stale generators will not be removed
2173 // as to be replaced by newly constructed ones.
2174 // Do note that it is not clear that this will happen for any call
2175 // in Qt's documentation and this should work only because of the
2176 // form of the current codebase and the scoping of the
2177 // initialize-terminate calls. As such, this should be considered
2178 // a patchwork that may or may not be doing anything and that may
2179 // break due to changes in other parts of the codebase.
2180 //
2181 // This is still to be considered temporary as the whole
2182 // initialize-terminate idiom must be removed from the codebase.
2183 s_generators.clear();
2184
2185 s_fmtLeftMaps.clear();
2186 s_fmtRightMaps.clear();
2187 s_outDir.clear();
2188}
2189
2190void Generator::terminateGenerator() {}
2191
2192/*!
2193 Trims trailing whitespace off the \a string and returns
2194 the trimmed string.
2195 */
2196QString Generator::trimmedTrailing(const QString &string, const QString &prefix,
2197 const QString &suffix)
2198{
2199 QString trimmed = string;
2200 while (trimmed.size() > 0 && trimmed[trimmed.size() - 1].isSpace())
2201 trimmed.truncate(pos: trimmed.size() - 1);
2202
2203 trimmed.append(s: suffix);
2204 trimmed.prepend(s: prefix);
2205 return trimmed;
2206}
2207
2208QString Generator::typeString(const Node *node)
2209{
2210 switch (node->nodeType()) {
2211 case NodeType::Namespace:
2212 return "namespace";
2213 case NodeType::Class:
2214 return "class";
2215 case NodeType::Struct:
2216 return "struct";
2217 case NodeType::Union:
2218 return "union";
2219 case NodeType::QmlType:
2220 case NodeType::QmlValueType:
2221 return "type";
2222 case NodeType::Page:
2223 return "documentation";
2224 case NodeType::Enum:
2225 return "enum";
2226 case NodeType::Typedef:
2227 case NodeType::TypeAlias:
2228 return "typedef";
2229 case NodeType::Function: {
2230 const auto fn = static_cast<const FunctionNode *>(node);
2231 switch (fn->metaness()) {
2232 case FunctionNode::QmlSignal:
2233 return "signal";
2234 case FunctionNode::QmlSignalHandler:
2235 return "signal handler";
2236 case FunctionNode::QmlMethod:
2237 return "method";
2238 case FunctionNode::MacroWithParams:
2239 case FunctionNode::MacroWithoutParams:
2240 return "macro";
2241 default:
2242 break;
2243 }
2244 return "function";
2245 }
2246 case NodeType::Property:
2247 case NodeType::QmlProperty:
2248 return "property";
2249 case NodeType::Module:
2250 case NodeType::QmlModule:
2251 return "module";
2252 case NodeType::SharedComment: {
2253 const auto &collective = static_cast<const SharedCommentNode *>(node)->collective();
2254 return collective.first()->nodeTypeString();
2255 }
2256 default:
2257 return "documentation";
2258 }
2259}
2260
2261void Generator::unknownAtom(const Atom *atom)
2262{
2263 Location::internalError(QStringLiteral("unknown atom type '%1' in %2 generator")
2264 .arg(args: atom->typeString(), args: format()));
2265}
2266
2267/*!
2268 * Generate the CMake requisite for the node \a cn, i.e. the the find_package and target_link_libraries
2269 * calls to use it.
2270 *
2271 * If only cmakepackage is set it will look like
2272 *
2273 * \badcode
2274 * find_package(Foo REQUIRED)
2275 * target_link_libraries(mytarget PRIVATE Foo:Foo)
2276 * \endcode
2277 *
2278 * If no cmakepackage is set Qt6 is assumed.
2279 *
2280 * If cmakecomponent is set it will look like
2281 *
2282 * \badcode
2283 * find_package(Qt6 REQUIRED COMPONENTS Bar)
2284 * target_link_libraries(mytarget PRIVATE Qt6::Bar)
2285 * \endcode
2286 *
2287 * If cmaketargetitem is set the item in target_link_libraries will be set accordingly
2288 *
2289 * \badcode
2290 * find_package(Qt6 REQUIRED COMPONENTS Bar)
2291 * target_link_libraries(mytarget PRIVATE My::Target)
2292 * \endcode
2293 *
2294 * Returns a pair consisting of the find package line and link libraries line.
2295 *
2296 * If no sensible requisite can be created (i.e. both cmakecomponent and cmakepackage are unset)
2297 * \c std::nullopt is returned.
2298 */
2299std::optional<std::pair<QString, QString>> Generator::cmakeRequisite(const CollectionNode *cn)
2300{
2301 if (!cn || (cn->cmakeComponent().isEmpty() && cn->cmakePackage().isEmpty())) {
2302 return {};
2303 }
2304
2305 const QString package =
2306 cn->cmakePackage().isEmpty() ? "Qt" + QString::number(QT_VERSION_MAJOR) : cn->cmakePackage();
2307
2308 QString findPackageText;
2309 if (cn->cmakeComponent().isEmpty()) {
2310 findPackageText = "find_package(" + package + " REQUIRED)";
2311 } else {
2312 findPackageText = "find_package(" + package + " REQUIRED COMPONENTS " + cn->cmakeComponent() + ")";
2313 }
2314
2315 QString targetText;
2316 if (cn->cmakeTargetItem().isEmpty()) {
2317 if (cn->cmakeComponent().isEmpty()) {
2318 targetText = package + "::" + package;
2319 } else {
2320 targetText = package + "::" + cn->cmakeComponent();
2321 }
2322 } else {
2323 targetText = cn->cmakeTargetItem();
2324 }
2325
2326 const QString targetLinkLibrariesText = "target_link_libraries(mytarget PRIVATE " + targetText + ")";
2327 const QStringList cmakeInfo { findPackageText, targetLinkLibrariesText };
2328
2329 return std::make_pair(x&: findPackageText, y: targetLinkLibrariesText);
2330}
2331
2332/*!
2333 \brief Adds a formatted link to the specified \a text stream.
2334
2335 This function creates a sequence of Atom objects that together form a link
2336 and appends them to the \a text. The \a nodeRef parameter specifies the
2337 target of the link (typically obtained via stringForNode()), and \a linkText
2338 specifies the visible text for the link.
2339
2340 \sa Atom, stringForNode()
2341*/
2342void Generator::addNodeLink(Text &text, const QString &nodeRef, const QString &linkText) {
2343 text << Atom(Atom::LinkNode, nodeRef)
2344 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
2345 << Atom(Atom::String, linkText)
2346 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
2347}
2348
2349/*!
2350 \overload
2351
2352 This convenience overload automatically obtains the node reference string
2353 using stringForNode(). If \a linkText is empty, the node's name is used as
2354 the link text; otherwise, the specified \a linkText is used.
2355
2356 \sa stringForNode()
2357*/
2358void Generator::addNodeLink(Text &text, const INode *node, const QString &linkText) {
2359 addNodeLink(
2360 text,
2361 nodeRef: Utilities::stringForNode(node),
2362 linkText: linkText.isEmpty() ? node->name() : linkText
2363 );
2364}
2365
2366/*!
2367 Generates a contextual code snippet for connecting to an overloaded signal or slot.
2368 Returns an empty string if the function is not a signal or slot.
2369*/
2370QString Generator::generateOverloadSnippet(const FunctionNode *func)
2371{
2372 if (!func || (!func->isSignal() && !func->isSlot()))
2373 return QString();
2374
2375 QString className = func->parent()->name();
2376 QString functionName = func->name();
2377 QString parameters = func->parameters().generateTypeList();
2378
2379 QString objectName = generateObjectName(className);
2380
2381 QString snippet = QString(
2382 "// Connect using qOverload:\n"
2383 "connect(%1, qOverload<%2>(&%3::%4),\n"
2384 " receiver, &ReceiverClass::slot);\n\n"
2385 "// Or using a lambda:\n"
2386 "connect(%1, qOverload<%2>(&%3::%4),\n"
2387 " this, [](%5) { /* handle %4 */ });")
2388 .arg(args&: objectName, args&: parameters, args&: className, args&: functionName,
2389 args: func->parameters().generateTypeAndNameList());
2390
2391 return snippet;
2392}
2393
2394/*!
2395 Generates an appropriate object name for code snippets based on the class name.
2396 Converts class names like "QComboBox" to "comboBox".
2397*/
2398QString Generator::generateObjectName(const QString &className)
2399{
2400 QString name = className;
2401
2402 if (name.startsWith(c: 'Q') && name.length() > 1)
2403 name.remove(i: 0, len: 1);
2404
2405 if (!name.isEmpty())
2406 name[0] = name[0].toLower();
2407
2408 return name;
2409}
2410
2411
2412QT_END_NAMESPACE
2413

source code of qttools/src/qdoc/qdoc/src/qdoc/generator.cpp