// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "generator.h"

#include "access.h"
#include "aggregate.h"
#include "classnode.h"
#include "codemarker.h"
#include "codeparser.h"
#include "collectionnode.h"
#include "comparisoncategory.h"
#include "config.h"
#include "doc.h"
#include "editdistance.h"
#include "enumnode.h"
#include "examplenode.h"
#include "functionnode.h"
#include "inclusionfilter.h"
#include "inclusionpolicy.h"
#include "inode.h"
#include "node.h"
#include "openedlist.h"
#include "outputdirectory.h"
#include "outputproducerregistry.h"
#include "propertynode.h"
#include "qdocdatabase.h"
#include "qmltypenode.h"
#include "qmlpropertynode.h"
#include "quoter.h"
#include "sharedcommentnode.h"
#include "tokenizer.h"
#include "typedefnode.h"
#include "utilities.h"
#include "textutils.h"

#include <QtCore/qdebug.h>
#include <QtCore/qdir.h>
#include <QtCore/qregularexpression.h>

#ifndef QT_BOOTSTRAPPED
#    include "QtCore/qurl.h"
#endif

#include <string>
#include <utility>

using namespace std::literals::string_literals;

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

Generator *Generator::s_currentGenerator;
QMap<QString, QMap<QString, QString>> Generator::s_fmtLeftMaps;
QMap<QString, QMap<QString, QString>> Generator::s_fmtRightMaps;
QList<Generator *> Generator::s_generators;
QString Generator::s_outDir;
QString Generator::s_imagesOutDir;
QString Generator::s_outSubdir;
QStringList Generator::s_outFileNames;
QSet<QString> Generator::s_trademarks;
QSet<QString> Generator::s_outputFormats;
QHash<QString, QString> Generator::s_outputPrefixes;
QHash<QString, QString> Generator::s_outputSuffixes;
QString Generator::s_project;
bool Generator::s_noLinkErrors = false;
bool Generator::s_autolinkErrors = false;
bool Generator::s_redirectDocumentationToDevNull = false;
bool Generator::s_useOutputSubdirs = true;
QmlTypeNode *Generator::s_qmlTypeContext = nullptr;

static QRegularExpression tag("</?@[^>]*>");
static QLatin1String amp("&amp;");
static QLatin1String gt("&gt;");
static QLatin1String lt("&lt;");
static QLatin1String quot("&quot;");

/*!
    Returns the set of template parameter names inherited from the parent
    scope chain of \a node. This includes template parameters from enclosing
    class templates, which are visible but not required to be documented
    in nested classes or member functions.
*/
static QSet<QString> inheritedTemplateParamNames(const Node *node)
{
    QSet<QString> names;
    for (const Node *p = node->parent(); p; p = p->parent()) {
        if (p->templateDecl())
            names.unite(p->templateDecl()->parameterNames());
    }
    return names;
}

/*!
    \enum ValidationContext
    Selects warning message wording based on documentation context.

    \value FunctionDoc Warns "No such parameter" (function docs may reference
           both function parameters and template parameters).
    \value TemplateDoc Warns "No such template parameter" (template class/alias
           docs reference only template parameters).
*/
enum class ValidationContext { FunctionDoc, TemplateDoc };

/*!
    Warns about documented parameter names in \a node that don't exist in
    \a allowedNames. Uses \a context to select appropriate wording.
*/
static void warnAboutUnknownDocumentedParams(const Node *node,
                                             const QSet<QString> &documentedNames,
                                             const QSet<QString> &allowedNames,
                                             ValidationContext context)
{
    for (const auto &name : documentedNames) {
        if (!allowedNames.contains(name) && CodeParser::isWorthWarningAbout(node->doc())) {
            const auto message = (context == ValidationContext::TemplateDoc)
                    ? "No such template parameter '%1' in %2"_L1
                    : "No such parameter '%1' in %2"_L1;
            node->doc().location().warning(message.arg(name, node->plainFullName()),
                                           suggestName(name, allowedNames));
        }
    }
}

/*!
  Constructs the generator base class. Prepends the newly
  constructed generator to the list of output generators.
  Sets a pointer to the QDoc database singleton, which is
  available to the generator subclasses.
 */
Generator::Generator(FileResolver& file_resolver)
    : file_resolver{file_resolver}
{
    m_qdb = QDocDatabase::qdocDB();
    s_generators.prepend(this);
}

/*!
  Destroys the generator after removing it from the list of
  output generators.
 */
Generator::~Generator()
{
    s_generators.removeAll(this);
}

void Generator::appendFullName(Text &text, const Node *apparentNode, const Node *relative,
                               const Node *actualNode)
{
    if (actualNode == nullptr)
        actualNode = apparentNode;

    addNodeLink(text, actualNode, apparentNode->plainFullName(relative));
}

void Generator::appendFullName(Text &text, const Node *apparentNode, const QString &fullName,
                               const Node *actualNode)
{
    if (actualNode == nullptr)
        actualNode = apparentNode;

    addNodeLink(text, actualNode, fullName);
}

/*!
  Append the signature for the function named in \a node to
  \a text, so that is a link to the documentation for that
  function.
 */
void Generator::appendSignature(Text &text, const Node *node)
{
    addNodeLink(text, node, node->signature(Node::SignaturePlain));
}

/*!
  Generate a bullet list of function signatures. The function
  nodes are in \a nodes. It uses the \a relative node and the
  \a marker for the generation.
 */
void Generator::signatureList(const NodeList &nodes, const Node *relative, CodeMarker *marker)
{
    Text text;
    int count = 0;
    text << Atom(Atom::ListLeft, QString("bullet"));
    for (const auto &node : nodes) {
        text << Atom(Atom::ListItemNumber, QString::number(++count));
        text << Atom(Atom::ListItemLeft, QString("bullet"));
        appendSignature(text, node);
        text << Atom(Atom::ListItemRight, QString("bullet"));
    }
    text << Atom(Atom::ListRight, QString("bullet"));
    generateText(text, relative, marker);
}

int Generator::appendSortedNames(Text &text, const ClassNode *cn, const QList<RelatedClass> &rc)
{
    QMap<QString, Text> classMap;
    for (const auto &relatedClass : rc) {
        ClassNode *rcn = relatedClass.m_node;
        if (rcn && rcn->isInAPI()) {
            Text className;
            appendFullName(className, rcn, cn);
            classMap[className.toString().toLower()] = className;
        }
    }

    int index = 0;
    const QStringList classNames = classMap.keys();
    for (const auto &className : classNames) {
        text << classMap[className];
        text << TextUtils::comma(index++, classNames.size());
    }
    return index;
}

int Generator::appendSortedQmlNames(Text &text, const Node *base, const QStringList &knownTypes,
                                    const NodeList &subs)
{
    QMap<QString, Text> classMap;

    QStringList typeNames(knownTypes);
    for (const auto sub : subs)
        typeNames << sub->name();

    for (const auto sub : subs) {
        Text full_name;
        appendFullName(full_name, sub, base);
        // Disambiguate with '(<QML module name>)' if there are clashing type names
        if (typeNames.count(sub->name()) > 1)
            full_name << Atom(Atom::String, " (%1)"_L1.arg(sub->logicalModuleName()));
        classMap[full_name.toString().toLower()] = full_name;
    }

    int index = 0;
    const auto &names = classMap.keys();
    for (const auto &name : names)
        text << classMap[name] << TextUtils::comma(index++, names.size());
    return index;
}

/*!
  Creates the file named \a fileName in the output directory
  and returns a QFile pointing to this file. In particular,
  this method deals with errors when opening the file:
  the returned QFile is always valid and can be written to.

  \sa beginSubPage()
 */
QFile *Generator::openSubPageFile(const PageNode *node, const QString &fileName)
{
    // Skip generating a warning for license attribution pages, as their source
    // is generated by qtattributionsscanner and may potentially include duplicates.
    // NOTE: Depending on the value of the `QtParts` field in qt_attribution.json files,
    // qtattributionsscanner may not use the \attribution QDoc command for the page
    // (by design). Therefore, check also filename.
    if (s_outFileNames.contains(fileName) && !node->isAttribution() && !fileName.contains("-attribution-"_L1))
        node->location().warning("Already generated %1 for this project"_L1.arg(fileName));

    QString path = outputDir() + QLatin1Char('/') + fileName;

    const auto &outPath = s_redirectDocumentationToDevNull ? QStringLiteral("/dev/null") : path;
    auto outFile = new QFile(outPath);

    if (!s_redirectDocumentationToDevNull && outFile->exists()) {
        const QString warningText {"Output file already exists, overwriting %1"_L1.arg(outFile->fileName())};
        if (qEnvironmentVariableIsSet("QDOC_ALL_OVERWRITES_ARE_WARNINGS"))
            node->location().warning(warningText);
        else
            qCDebug(lcQdoc) << qUtf8Printable(warningText);
    }

    if (!outFile->open(QFile::WriteOnly | QFile::Text)) {
        node->location().fatal(
                QStringLiteral("Cannot open output file '%1'").arg(outFile->fileName()));
    }

    qCDebug(lcQdoc, "Writing: %s", qPrintable(path));
    s_outFileNames << fileName;
    s_trademarks.clear();
    return outFile;
}

/*!
  Creates the file named \a fileName in the output directory.
  Attaches a QTextStream to the created file, which is written
  to all over the place using out().
 */
void Generator::beginSubPage(const PageNode *node, const QString &fileName)
{
    QFile *outFile = openSubPageFile(static_cast<const PageNode*>(node), fileName);
    auto *out = new QTextStream(outFile);
    outStreamStack.push(out);
}

/*!
  Flush the text stream associated with the subpage, and
  then pop it off the text stream stack and delete it.
  This terminates output of the subpage.
 */
void Generator::endSubPage()
{
    outStreamStack.top()->flush();
    delete outStreamStack.top()->device();
    delete outStreamStack.pop();
}

QString Generator::fileBase(const Node *node) const
{
    if (!node->isPageNode() && !node->isCollectionNode())
        node = node->parent();

    if (node->hasFileNameBase())
        return node->fileNameBase();

    QString result = Utilities::computeFileBase(
        node, s_project,
        [](const Node *n) { return outputPrefix(n); },
        [](const Node *n) { return outputSuffix(n); });

    const_cast<Node *>(node)->setFileNameBase(result);
    return result;
}

/*!
  Constructs an href link from an example file name, which
  is a \a path to the example file. If \a fileExt is empty
  (default value), retrieve the file extension from
  the generator.
 */
QString Generator::linkForExampleFile(const QString &path, const QString &fileExt) const
{
    return Utilities::linkForExampleFile(path, s_project, fileExt.isEmpty() ? fileExtension() : fileExt);
}

/*!
    Helper function to construct a title for a file or image page
    included in an example.
*/
QString Generator::exampleFileTitle(const ExampleNode *relative, const QString &fileName)
{
    return Utilities::exampleFileTitle(relative->files(), relative->images(), fileName);
}

/*!
  If the \a node has a URL, return the URL as the file name.
  Otherwise, construct the file name from the fileBase() and
  either the provided \a extension or fileExtension(), and
  return the constructed name.
 */
QString Generator::fileName(const Node *node, const QString &extension) const
{
    if (!node->url().isEmpty())
        return node->url();

    // Special case for simple page nodes (\page commands) with explicit
    // non-.html extensions. Use the normalized fileBase() but preserve
    // user specified extension
    if (node->isTextPageNode() && !node->isCollectionNode() && extension.isNull()) {
        QFileInfo originalName(node->name());
        QString suffix = originalName.suffix();
        if (!suffix.isEmpty() && suffix != "html") {
            // User specified a non-.html extension - use normalized base + original extension
            QString name = fileBase(node);
            return name + QLatin1Char('.') + suffix;
        }
    }

    QString name = fileBase(node) + QLatin1Char('.');
    return name + (extension.isNull() ? fileExtension() : extension);
}

/*!
  Clean the given \a ref to be used as an HTML anchor or an \c xml:id.
  If \a xmlCompliant is set to \c true, a stricter process is used, as XML
  is more rigorous in what it accepts. Otherwise, if \a xmlCompliant is set to
  \c false, the basic HTML transformations are applied.

  More specifically, only XML NCNames are allowed
  (https://www.w3.org/TR/REC-xml-names/#NT-NCName).
 */
QString Generator::cleanRef(const QString &ref, bool xmlCompliant)
{
    // XML-compliance is ensured in two ways:
    // - no digit (0-9) at the beginning of an ID (many IDs do not respect this property)
    // - no colon (:) anywhere in the ID (occurs very rarely)

    QString clean;

    if (ref.isEmpty())
        return clean;

    clean.reserve(ref.size() + 20);
    const QChar c = ref[0];
    const uint u = c.unicode();

    if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (!xmlCompliant && u >= '0' && u <= '9')) {
        clean += c;
    } else if (xmlCompliant && u >= '0' && u <= '9') {
        clean += QLatin1Char('A') + c;
    } else if (u == '~') {
        clean += "dtor.";
    } else if (u == '_') {
        clean += "underscore.";
    } else {
        clean += QLatin1Char('A');
    }

    for (int i = 1; i < ref.size(); i++) {
        const QChar c = ref[i];
        const uint u = c.unicode();
        if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9') || u == '-'
            || u == '_' || (xmlCompliant && u == ':') || u == '.') {
            clean += c;
        } else if (c.isSpace()) {
            clean += QLatin1Char('-');
        } else if (u == '!') {
            clean += "-not";
        } else if (u == '&') {
            clean += "-and";
        } else if (u == '<') {
            clean += "-lt";
        } else if (u == '=') {
            clean += "-eq";
        } else if (u == '>') {
            clean += "-gt";
        } else if (u == '#') {
            clean += QLatin1Char('#');
        } else {
            clean += QLatin1Char('-');
            clean += QString::number(static_cast<int>(u), 16);
        }
    }
    return clean;
}

QMap<QString, QString> &Generator::formattingLeftMap()
{
    return s_fmtLeftMaps[format()];
}

QMap<QString, QString> &Generator::formattingRightMap()
{
    return s_fmtRightMaps[format()];
}

/*!
  Returns the full document location.
 */
QString Generator::fullDocumentLocation(const Node *node) const
{
    if (node == nullptr)
        return QString();
    if (!node->url().isEmpty())
        return node->url();

    QString parentName;
    QString anchorRef;

    if (node->isNamespace()) {
        /*
          The root namespace has no name - check for this before creating
          an attribute containing the location of any documentation.
        */
        if (!fileBase(node).isEmpty())
            parentName = fileBase(node) + QLatin1Char('.') + fileExtension();
        else
            return QString();
    } else if (node->isQmlType()) {
        return fileBase(node) + QLatin1Char('.') + fileExtension();
    } else if (node->isTextPageNode() || node->isCollectionNode()) {
        parentName = fileBase(node) + QLatin1Char('.') + fileExtension();
    } else if (fileBase(node).isEmpty())
        return QString();

    Node *parentNode = nullptr;

    if ((parentNode = node->parent())) {
        // use the parent's name unless the parent is the root namespace
        if (!node->parent()->isNamespace() || !node->parent()->name().isEmpty())
            parentName = fullDocumentLocation(node->parent());
    }

    switch (node->nodeType()) {
    case NodeType::Class:
    case NodeType::Struct:
    case NodeType::Union:
    case NodeType::Namespace:
    case NodeType::Proxy:
        parentName = fileBase(node) + QLatin1Char('.') + fileExtension();
        break;
    case NodeType::Function: {
        const auto *fn = static_cast<const FunctionNode *>(node);
        switch (fn->metaness()) {
        case Metaness::QmlSignal:
            anchorRef = QLatin1Char('#') + node->name() + "-signal";
            break;
        case Metaness::QmlSignalHandler:
            anchorRef = QLatin1Char('#') + node->name() + "-signal-handler";
            break;
        case Metaness::QmlMethod:
            anchorRef = QLatin1Char('#') + node->name() + "-method";
            break;
        default:
            if (fn->isDtor())
                anchorRef = "#dtor." + fn->name().mid(1);
            else if (const auto *p = fn->primaryAssociatedProperty(); p && fn->doc().isEmpty())
                return fullDocumentLocation(p);
            else if (fn->overloadNumber() > 0)
                anchorRef = QLatin1Char('#') + cleanRef(fn->name()) + QLatin1Char('-')
                        + QString::number(fn->overloadNumber());
            else
                anchorRef = QLatin1Char('#') + cleanRef(fn->name());
            break;
        }
        break;
    }
    /*
      Use node->name() instead of fileBase(node) as
      the latter returns the name in lower-case. For
      HTML anchors, we need to preserve the case.
    */
    case NodeType::Enum:
    case NodeType::QmlEnum:
        anchorRef = QLatin1Char('#') + node->name() + "-enum";
        break;
    case NodeType::Typedef: {
        const auto *tdef = static_cast<const TypedefNode *>(node);
        if (tdef->associatedEnum())
            return fullDocumentLocation(tdef->associatedEnum());
    } Q_FALLTHROUGH();
    case NodeType::TypeAlias:
        anchorRef = QLatin1Char('#') + node->name() + "-typedef";
        break;
    case NodeType::Property:
        anchorRef = QLatin1Char('#') + node->name() + "-prop";
        break;
    case NodeType::SharedComment: {
        if (!node->isPropertyGroup())
            break;
    } Q_FALLTHROUGH();
    case NodeType::QmlProperty:
        if (node->isAttached())
            anchorRef = QLatin1Char('#') + node->name() + "-attached-prop";
        else
            anchorRef = QLatin1Char('#') + node->name() + "-prop";
        break;
    case NodeType::Variable:
        anchorRef = QLatin1Char('#') + node->name() + "-var";
        break;
    case NodeType::QmlType:
    case NodeType::Page:
    case NodeType::Group:
    case NodeType::HeaderFile:
    case NodeType::Module:
    case NodeType::QmlModule: {
        parentName = fileBase(node);
        parentName.replace(QLatin1Char('/'), QLatin1Char('-'))
                .replace(QLatin1Char('.'), QLatin1Char('-'));
        parentName += QLatin1Char('.') + fileExtension();
    } break;
    default:
        break;
    }

    if (!node->isClassNode() && !node->isNamespace()) {
        if (node->isDeprecated())
            parentName.replace(QLatin1Char('.') + fileExtension(),
                               "-obsolete." + fileExtension());
    }

    return parentName.toLower() + anchorRef;
}

/*!
    Generates text for a "see also" list for the given \a node and \a marker
    if a list has been defined.

    Check for links to the node containing the \sa command, looking for empty
    ref fields to ensure that a link is referring to the node itself and not
    a different section of a larger document.
*/
void Generator::generateAlsoList(const Node *node, CodeMarker *marker)
{
    QList<Text> alsoList = node->doc().alsoList();
    supplementAlsoList(node, alsoList);

    if (!alsoList.isEmpty()) {
        Text text;
        text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) << "See also "
             << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD);

        QSet<QString> used;
        QList<Text> items;
        for (const auto &also : std::as_const(alsoList)) {
            // Every item starts with a link atom.
            const Atom *atom = also.firstAtom();
            QString link = atom->string();
            if (!used.contains(link)) {
                items.append(also);
                used.insert(link);

                QString ref;
                if (m_qdb->findNodeForAtom(atom, node, ref) == node && ref.isEmpty())
                    node->doc().location().warning("Redundant link to self in \\sa command for %1"_L1.arg(node->name()));
            }
        }

        int i = 0;
        for (const auto &also : std::as_const(items))
            text << also << TextUtils::separator(i++, items.size());

        text << Atom::ParaRight;
        generateText(text, node, marker);
    }
}

const Atom *Generator::generateAtomList(const Atom *atom, const Node *relative, CodeMarker *marker,
                                        bool generate, int &numAtoms)
{
    while (atom != nullptr) {
        if (atom->type() == Atom::FormatIf) {
            int numAtoms0 = numAtoms;
            bool rightFormat = canHandleFormat(atom->string());
            atom = generateAtomList(atom->next(), relative, marker, generate && rightFormat,
                                    numAtoms);
            if (atom == nullptr)
                return nullptr;

            if (atom->type() == Atom::FormatElse) {
                ++numAtoms;
                atom = generateAtomList(atom->next(), relative, marker, generate && !rightFormat,
                                        numAtoms);
                if (atom == nullptr)
                    return nullptr;
            }

            if (atom->type() == Atom::FormatEndif) {
                if (generate && numAtoms0 == numAtoms) {
                    relative->location().warning(QStringLiteral("Output format %1 not handled %2")
                                                         .arg(format(), outFileName()));
                    Atom unhandledFormatAtom(Atom::UnhandledFormat, format());
                    generateAtomList(&unhandledFormatAtom, relative, marker, generate, numAtoms);
                }
                atom = atom->next();
            }
        } else if (atom->type() == Atom::FormatElse || atom->type() == Atom::FormatEndif) {
            return atom;
        } else {
            int n = 1;
            if (generate) {
                n += generateAtom(atom, relative, marker);
                numAtoms += n;
            }
            while (n-- > 0)
                atom = atom->next();
        }
    }
    return nullptr;
}


/*!
  Generate the body of the documentation from the qdoc comment
  found with the entity represented by the \a node.
 */
void Generator::generateBody(const Node *node, CodeMarker *marker)
{
    const FunctionNode *fn = node->isFunction() ? static_cast<const FunctionNode *>(node) : nullptr;
    if (!node->hasDoc()) {
        /*
          Test for special function, like a destructor or copy constructor,
          that has no documentation.
        */
        if (fn) {
            if (fn->isDtor()) {
                Text text;
                text << "Destroys the instance of ";
                text << fn->parent()->name() << ".";
                if (fn->isVirtual())
                    text << " The destructor is virtual.";
                out() << "<p>";
                generateText(text, node, marker);
                out() << "</p>";
            } else if (fn->isCtor()) {
                Text text;
                text << "Default-constructs an instance of "
                     << fn->parent()->name() << ".";
                out() << "<p>";
                generateText(text, node, marker);
                out() << "</p>";
            } else if (fn->isCCtor()) {
                Text text;
                text << "Copy-constructs an instance of "
                     << fn->parent()->name() << ".";
                out() << "<p>";
                generateText(text, node, marker);
                out() << "</p>";
            } else if (fn->isMCtor()) {
                Text text;
                text << "Move-constructs an instance of "
                     << fn->parent()->name() << ".";
                out() << "<p>";
                generateText(text, node, marker);
                out() << "</p>";
            } else if (fn->isCAssign()) {
                Text text;
                text << "Copy-assigns "
                     << Atom(Atom::FormattingLeft, ATOM_FORMATTING_PARAMETER)
                     << "other" << Atom(Atom::FormattingRight, ATOM_FORMATTING_PARAMETER)
                     << " to this " << fn->parent()->name() << " instance.";
                out() << "<p>";
                generateText(text, node, marker);
                out() << "</p>";
            } else if (fn->isMAssign()) {
                Text text;
                text << "Move-assigns "
                     << Atom(Atom::FormattingLeft, ATOM_FORMATTING_PARAMETER)
                     << "other" << Atom(Atom::FormattingRight, ATOM_FORMATTING_PARAMETER)
                     << " to this " << fn->parent()->name() << " instance.";
                out() << "<p>";
                generateText(text, node, marker);
                out() << "</p>";
            } else if (!node->isWrapper() && !node->isMarkedReimp()) {
                const InclusionPolicy policy = Config::instance().createInclusionPolicy();
                const NodeContext context = node->createContext();
                if (!fn->isIgnored() && InclusionFilter::requiresDocumentation(policy, context)) // undocumented functions added by Q_OBJECT
                    node->location().warning(QStringLiteral("No documentation for '%1'")
                                                     .arg(node->plainSignature()));
            }
        } else if (!node->isWrapper() && !node->isMarkedReimp()) {
            // Don't require documentation of things defined in Q_GADGET
            const InclusionPolicy policy = Config::instance().createInclusionPolicy();
            const NodeContext context = node->createContext();
            if (node->name() != QLatin1String("QtGadgetHelper") && InclusionFilter::requiresDocumentation(policy, context))
                node->location().warning(
                        QStringLiteral("No documentation for '%1'").arg(node->plainSignature()));
        }
    } else if (!node->isSharingComment()) {
        // Reimplements clause and type alias info precede body text
        if (fn && !fn->overridesThis().isEmpty())
            generateReimplementsClause(fn, marker);
        else if (node->isProperty()) {
            if (static_cast<const PropertyNode *>(node)->propertyType() != PropertyNode::PropertyType::StandardProperty)
                generateAddendum(node, BindableProperty, marker);
        }

        if (!generateText(node->doc().body(), node, marker)) {
            if (node->isMarkedReimp())
                return;
        }

        if (fn) {
            if (fn->isQmlSignal())
                generateAddendum(node, QmlSignalHandler, marker);
            if (fn->isPrivateSignal())
                generateAddendum(node, PrivateSignal, marker);
            if (fn->isInvokable())
                generateAddendum(node, Invokable, marker);
            if (fn->hasAssociatedProperties())
                generateAddendum(node, AssociatedProperties, marker);
            if (fn->hasOverloads() && fn->doc().hasOverloadCommand()
                && !fn->isSignal() && !fn->isSlot())
                generateAddendum(node, OverloadNote, marker, AdmonitionPrefix::None);
        }

        // Generate warnings
        if (node->isEnumType()) {
            const auto *enume = static_cast<const EnumNode *>(node);

            QSet<QString> definedItems;
            const QList<EnumItem> &items = enume->items();
            for (const auto &item : items)
                definedItems.insert(item.name());

            const auto &documentedItemList = enume->doc().enumItemNames();
            QSet<QString> documentedItems(documentedItemList.cbegin(), documentedItemList.cend());
            const QSet<QString> allItems = definedItems + documentedItems;
            if (allItems.size() > definedItems.size()
                || allItems.size() > documentedItems.size()) {
                for (const auto &it : allItems) {
                    if (!definedItems.contains(it)) {
                        node->doc().location().warning(
                                QStringLiteral("No such enum item '%1' in %2")
                                        .arg(it, node->plainFullName()),
                                QStringLiteral("Maybe you meant '%1'?")
                                        .arg(suggestName(it, definedItems, documentedItems)));
                    } else if (!documentedItems.contains(it)) {
                        node->doc().location().warning(
                                QStringLiteral("Undocumented enum item '%1' in %2")
                                        .arg(it, node->plainFullName()));
                    }
                }
            }
        } else if (fn) {
            // Build name environment with visibility vs. responsibility distinction:
            // - requiredNames: names that must be documented (function params + API-significant template params)
            // - allowedNames: all names that can be referenced (includes type template params + inherited)
            //
            // For functions, type template parameters (typename T) are not required because
            // they typically serve to type function parameters - documenting the function
            // parameter implicitly covers the template parameter's role. Only non-type and
            // template-template parameters are required as they carry independent meaning.
            const QSet<QString> requiredFunctionParams = fn->parameters().getNames();
            const QSet<QString> requiredTemplateParams = fn->templateDecl()
                    ? fn->templateDecl()->requiredParameterNamesForFunctions()
                    : QSet<QString>{};
            const QSet<QString> requiredNames = requiredFunctionParams + requiredTemplateParams;

            // All template parameters (including type params and inherited from parent chain)
            // are allowed to be referenced without "no such parameter" warnings
            const QSet<QString> ownTemplateParams = fn->templateDecl()
                    ? fn->templateDecl()->parameterNames()
                    : QSet<QString>{};
            const QSet<QString> allowedNames = requiredNames + ownTemplateParams
                    + inheritedTemplateParamNames(fn);

            const QSet<QString> documentedNames = fn->doc().parameterNames();

            // Warn about missing required parameters
            for (const auto &name : requiredNames) {
                if (!documentedNames.contains(name)) {
                    if (fn->isActive() || fn->isPreliminary()) {
                        // Require no parameter documentation for overrides and overloads,
                        // and only require it for non-overloaded constructors.
                        if (!fn->isMarkedReimp() && !fn->isOverload()
                            && !(fn->isSomeCtor() && fn->hasOverloads())) {
                            // Use appropriate wording based on parameter type
                            const bool isTemplateParam = requiredTemplateParams.contains(name);
                            fn->doc().location().warning(
                                    "Undocumented %1 '%2' in %3"_L1
                                            .arg(isTemplateParam ? "template parameter"_L1
                                                                 : "parameter"_L1,
                                                 name, node->plainFullName()));
                        }
                    }
                }
            }

            warnAboutUnknownDocumentedParams(fn, documentedNames, allowedNames,
                                             ValidationContext::FunctionDoc);
            /*
              This return value check should be implemented
              for all functions with a return type.
              mws 13/12/2018
            */
            if (!fn->isDeprecated() && fn->returnsBool() && !fn->isMarkedReimp()
                && !fn->isOverload()) {
                if (!fn->doc().body().contains("return"))
                    node->doc().location().warning(
                            QStringLiteral("Undocumented return value "
                                           "(hint: use 'return' or 'returns' in the text"));
            }
        } else if (node->isQmlProperty()) {
            if (auto *qpn = static_cast<const QmlPropertyNode *>(node); !qpn->validateDataType())
                qpn->doc().location().warning("Invalid QML property type: %1"_L1.arg(qpn->dataType()));
        } else if (node->templateDecl()) {
            // Template classes, type aliases, and other non-function template declarations
            // Use the same visibility vs. responsibility model as functions:
            // - requiredNames: template params declared on this node (must be documented)
            // - allowedNames: required + inherited from parent chain (can be referenced)
            const QSet<QString> requiredNames = node->templateDecl()->parameterNames();
            const QSet<QString> allowedNames = requiredNames + inheritedTemplateParamNames(node);
            const QSet<QString> documentedNames = node->doc().parameterNames();

            if (node->isActive() || node->isPreliminary()) {
                for (const auto &name : requiredNames) {
                    if (!documentedNames.contains(name) && CodeParser::isWorthWarningAbout(node->doc())) {
                        node->doc().location().warning(
                                "Undocumented template parameter '%1' in %2"_L1
                                        .arg(name, node->plainFullName()));
                    }
                }
            }

            warnAboutUnknownDocumentedParams(node, documentedNames, allowedNames,
                                             ValidationContext::TemplateDoc);
        }
    }
    generateEnumValuesForQmlReference(node, marker);
    generateRequiredLinks(node, marker);
}

/*!
  Generates either a link to the project folder for example \a node, or a list
  of links files/images if 'url.examples config' variable is not defined.

  Does nothing for non-example nodes.
*/
void Generator::generateRequiredLinks(const Node *node, CodeMarker *marker)
{
    if (!node->isExample())
        return;

    const auto *en = static_cast<const ExampleNode *>(node);
    QString exampleUrl{Config::instance().get(CONFIG_URL + Config::dot + CONFIG_EXAMPLES).asString()};

    if (exampleUrl.isEmpty()) {
        if (!en->noAutoList()) {
            generateFileList(en, marker, false); // files
            generateFileList(en, marker, true); // images
        }
    } else {
        generateLinkToExample(en, marker, exampleUrl);
    }
}

/*!
  Generates an external link to the project folder for example \a node.
  The path to the example replaces a placeholder '\1' character if
  one is found in the \a baseUrl string. If no such placeholder is found,
  the path is appended to \a baseUrl, after a '/' character if \a baseUrl did
  not already end in one.
*/
void Generator::generateLinkToExample(const ExampleNode *en, CodeMarker *marker,
                                      const QString &baseUrl)
{
    QString exampleUrl(baseUrl);
    QString link;
#ifndef QT_BOOTSTRAPPED
    link = QUrl(exampleUrl).host();
#endif
    if (!link.isEmpty())
        link.prepend(" @ ");
    link.prepend("Example project");

    const QLatin1Char separator('/');
    const QLatin1Char placeholder('\1');
    if (!exampleUrl.contains(placeholder)) {
        if (!exampleUrl.endsWith(separator))
            exampleUrl += separator;
        exampleUrl += placeholder;
    }

    // Construct a path to the example; <install path>/<example name>
    QString pathRoot;
    QStringMultiMap *metaTagMap = en->doc().metaTagMap();
    if (metaTagMap)
        pathRoot = metaTagMap->value(QLatin1String("installpath"));
    if (pathRoot.isEmpty())
        pathRoot = Config::instance().get(CONFIG_EXAMPLESINSTALLPATH).asString();
    QStringList path = QStringList() << pathRoot << en->name();
    path.removeAll(QString());

    Text text;
    text << Atom::ParaLeft
         << Atom(Atom::Link, exampleUrl.replace(placeholder, path.join(separator)))
         << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << Atom(Atom::String, link)
         << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom::ParaRight;

    generateText(text, nullptr, marker);
}

void Generator::addImageToCopy(const ExampleNode *en, const ResolvedFile& resolved_file)
{
    // TODO: [uncentralized-output-directory-structure]
    const QString prefix("/images/used-in-examples");

    // TODO: Generators probably should not need to keep track of which files were generated.
    // Understand if we really need this information and where it should
    // belong, considering that it should be part of whichever system
    // would actually store the file itself.
    s_outFileNames << prefix.mid(1) + "/" + resolved_file.get_query();

    const OutputDirectory outDir =
            OutputDirectory::ensure(s_outDir, en->location());
    const OutputDirectory imagesUsedInExamplesDir =
            outDir.ensureSubdir(prefix.mid(1), en->location());

    const QFileInfo fi{resolved_file.get_query()};
    const QString relativePath = fi.path();
    // QFileInfo::path() can return "." for files with no directory component
    const bool hasSubdir = !relativePath.isEmpty() && relativePath != "."_L1;
    const OutputDirectory imgOutDir =
            hasSubdir ? imagesUsedInExamplesDir.ensureSubdir(relativePath, en->location())
                      : imagesUsedInExamplesDir;

    const QString fileName = fi.fileName();
    Config::copyFile(en->location(), resolved_file.get_path(), fileName, imgOutDir.path());
}

// TODO: [multi-purpose-function-with-flag][generate-file-list]
// Avoid the use of a boolean flag to dispatch to the correct
// implementation trough branching.
// We always have to process both images and files, such that we
// should consider to remove the branching altogheter, performing both
// operations in a single call.
// Otherwise, if this turns out to be infeasible, complex or
// possibly-confusing, consider extracting the processing code outside
// the function and provide two higer-level dispathing functions for
// files and images.

/*!
  This function is called when the documentation for an example is
  being formatted. It outputs a list of files for the example, which
  can be the example's source files or the list of images used by the
  example. The images are copied into a subtree of
  \c{...doc/html/images/used-in-examples/...}
*/
void Generator::generateFileList(const ExampleNode *en, CodeMarker *marker, bool images)
{
    Text text;
    OpenedList openedList(OpenedList::Bullet);
    QString tag;
    QStringList paths;
    Atom::AtomType atomType = Atom::ExampleFileLink;

    if (images) {
        paths = en->images();
        tag = "Images:";
        atomType = Atom::ExampleImageLink;
    } else { // files
        paths = en->files();
        tag = "Files:";
    }
    std::sort(paths.begin(), paths.end(), Generator::comparePaths);

    text << Atom::ParaLeft << tag << Atom::ParaRight;
    text << Atom(Atom::ListLeft, openedList.styleString());

    for (const auto &path : std::as_const(paths)) {
        auto maybe_resolved_file{file_resolver.resolve(path)};
        if (!maybe_resolved_file) {
            // TODO: [uncentralized-admonition][failed-resolve-file]
            QString details = std::transform_reduce(
                file_resolver.get_search_directories().cbegin(),
                file_resolver.get_search_directories().cend(),
                u"Searched directories:"_s,
                std::plus(),
                [](const DirectoryPath &directory_path) -> QString { return u' ' + directory_path.value(); }
            );

            en->location().warning(u"(Generator)Cannot find file to quote from: %1"_s.arg(path), details);

            continue;
        }

        const auto &file{*maybe_resolved_file};
        if (images)
            addImageToCopy(en, file);
        else
            generateExampleFilePage(en, file, marker);

        openedList.next();
        text << Atom(Atom::ListItemNumber, openedList.numberString())
             << Atom(Atom::ListItemLeft, openedList.styleString()) << Atom::ParaLeft
             << Atom(atomType, file.get_query()) << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << file.get_query()
             << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom::ParaRight
             << Atom(Atom::ListItemRight, openedList.styleString());
    }
    text << Atom(Atom::ListRight, openedList.styleString());
    if (!paths.isEmpty())
        generateText(text, en, marker);
}

/*!
  Recursive writing of HTML files from the root \a node.
 */
void Generator::generateDocumentation(Node *node)
{
    if (!node->url().isNull())
        return;
    if (node->isIndexNode())
        return;
    const InclusionPolicy policy = Config::instance().createInclusionPolicy();
    const NodeContext context = node->createContext();
    if (!InclusionFilter::isIncluded(policy, context))
        return;
    if (node->isExternalPage())
        return;

    /*
      Obtain a code marker for the source file.
     */
    CodeMarker *marker = CodeMarker::markerForFileName(node->location().filePath());

    if (node->parent() != nullptr && node->isPageNode()) {
        PageNode *pageNode = static_cast<PageNode *>(node);
        if (pageNode->isCollectionNode()) {
            /*
              A collection node collects: groups, C++ modules, or QML
              modules. Testing for a CollectionNode must be done
              before testing for a TextPageNode because a
              CollectionNode is a PageNode at this point.

              Don't output an HTML page for the collection node unless
              the \group, \module, or \qmlmodule command was actually
              seen by qdoc in the qdoc comment for the node.

              A key prerequisite in this case is the call to
              mergeCollections(cn). We must determine whether this
              group, module or QML module has members in other
              modules. We know at this point that cn's members list
              contains only members in the current module. Therefore,
              before outputting the page for cn, we must search for
              members of cn in the other modules and add them to the
              members list.
            */
            auto *cn = static_cast<CollectionNode *>(node);
            if (cn->wasSeen()) {
                m_qdb->mergeCollections(cn);
                beginSubPage(pageNode, fileName(node));
                generateCollectionNode(cn, marker);
                endSubPage();
            } else if (cn->isGenericCollection()) {
                // Currently used only for the module's related orphans page
                // but can be generalized for other kinds of collections if
                // other use cases pop up.
                QString name = cn->name().toLower();
                name.replace(QChar(' '), QString("-"));
                QString filename =
                        cn->tree()->physicalModuleName() + "-" + name + "." + fileExtension();
                beginSubPage(pageNode, filename);
                generateGenericCollectionPage(cn, marker);
                endSubPage();
            }
        } else if (node->isTextPageNode()) {
            beginSubPage(pageNode, fileName(node));
            generatePageNode(pageNode, marker);
            endSubPage();
        } else if (node->isAggregate()) {
            if ((node->isClassNode() || node->isHeader() || node->isNamespace())
                && node->docMustBeGenerated()) {
                beginSubPage(pageNode, fileName(node));
                generateCppReferencePage(static_cast<Aggregate *>(node), marker);
                endSubPage();
            } else if (node->isQmlType()) {
                beginSubPage(pageNode, fileName(node));
                auto *qcn = static_cast<QmlTypeNode *>(node);
                generateQmlTypePage(qcn, marker);
                endSubPage();
            } else if (node->isProxyNode()) {
                beginSubPage(pageNode, fileName(node));
                generateProxyPage(static_cast<Aggregate *>(node), marker);
                endSubPage();
            }
        }
    }

    if (node->isAggregate()) {
        auto *aggregate = static_cast<Aggregate *>(node);
        const NodeList &children = aggregate->childNodes();
        for (auto *child : children) {
            if (child->isPageNode()) {
                generateDocumentation(child);
            } else if (!node->parent() && child->isInAPI() && !child->isRelatedNonmember()
                       && !child->doc().isAutoGenerated()) {
                // Warn if there are documented non-page-generating nodes in the root namespace
                child->location().warning(u"No documentation generated for %1 '%2' in global scope."_s
                    .arg(typeString(child), child->name()),
                            u"Maybe you forgot to use the '\\relates' command?"_s);
                child->setStatus(Status::DontDocument);
            } else if (child->isQmlModule() && !child->wasSeen()) {
                // An undocumented QML module that was constructed as a placeholder
                auto *qmlModule = static_cast<CollectionNode *>(child);
                for (const auto *member : qmlModule->members()) {
                    member->location().warning(
                        u"Undocumented QML module '%1' referred by type '%2' or its members"_s
                            .arg(qmlModule->name(), member->name()),
                                u"Maybe you forgot to document '\\qmlmodule %1'?"_s
                                    .arg(qmlModule->name()));
                }
            } else if (child->isQmlType() && !child->hasDoc()) {
                // A placeholder QML type with incorrect module identifier
                auto *qmlType = static_cast<QmlTypeNode *>(child);
                if (auto qmid = qmlType->logicalModuleName(); !qmid.isEmpty())
                    qmlType->location().warning(u"No such type '%1' in QML module '%2'"_s
                        .arg(qmlType->name(), qmid));
            }
        }
    }
}

void Generator::generateReimplementsClause(const FunctionNode *fn, CodeMarker *marker)
{
    if (fn->overridesThis().isEmpty() || !fn->parent()->isClassNode())
        return;

    auto *cn = static_cast<ClassNode *>(fn->parent());
    const FunctionNode *overrides = cn->findOverriddenFunction(fn);
    if (overrides && !overrides->isPrivate() && !overrides->parent()->isPrivate()) {
        if (overrides->hasDoc()) {
            Text text;
            text << Atom::ParaLeft << "Reimplements: ";
            QString fullName =
                    overrides->parent()->name()
                    + "::" + overrides->signature(Node::SignaturePlain);
            appendFullName(text, overrides->parent(), fullName, overrides);
            text << "." << Atom::ParaRight;
            generateText(text, fn, marker);
        } else {
            fn->doc().location().warning(
                    QStringLiteral("Illegal \\reimp; no documented virtual function for %1")
                            .arg(overrides->plainSignature()));
        }
        return;
    }
    const PropertyNode *sameName = cn->findOverriddenProperty(fn);
    if (sameName && sameName->hasDoc()) {
        Text text;
        text << Atom::ParaLeft << "Reimplements an access function for property: ";
        QString fullName = sameName->parent()->name() + "::" + sameName->name();
        appendFullName(text, sameName->parent(), fullName, sameName);
        text << "." << Atom::ParaRight;
        generateText(text, fn, marker);
    }
}

QString Generator::formatSince(const Node *node)
{
    QStringList since = node->since().split(QLatin1Char(' '));

    // If there is only one argument, assume it is the product version number.
    if (since.size() == 1) {
        const QString productName = Config::instance().get(CONFIG_PRODUCTNAME).asString();
        return productName.isEmpty() ? node->since() :  productName +  " " + since[0];
    }

    // Otherwise, use the original <project> <version> string.
    return node->since();
}

/*!
    \internal
    Returns a string representing status information of a \a node.

    If a status description is returned, it is one of:
    \list
      \li Custom status set explicitly in node's documentation using
          \c {\meta {status} {<description>}},
      \li 'Deprecated [since <version>]' (\\deprecated [<version>]),
      \li 'Until <version>',
      \li 'Preliminary' or the value of config variable `preliminary'
          (\\preliminary), or
      \li The description adopted from associated module's state:
          \c {\modulestate {<description>}}.
    \endlist

    Otherwise, returns \c std::nullopt.
*/
std::optional<QString> formatStatus(const Node *node, QDocDatabase *qdb)
{
    QString status;

    if (const auto metaMap = node->doc().metaTagMap(); metaMap) {
        status = metaMap->value("status");
        if (!status.isEmpty())
            return {status};
    }
    const auto &since = node->deprecatedSince();
    if (node->status() == Status::Deprecated) {
        status = u"Deprecated"_s;
        if (!since.isEmpty())
            status += " since %1"_L1.arg(since);
    } else if (!since.isEmpty()) {
        status = "Until %1"_L1.arg(since);
    } else if (node->status() == Status::Preliminary) {
        status = Config::instance().get(CONFIG_PRELIMINARY).asString();
    } else if (const auto collection = qdb->getModuleNode(node); collection) {
        status = collection->state();
    }

    return status.isEmpty() ? std::nullopt : std::optional(status);
}

void Generator::generateSince(const Node *node, CodeMarker *marker)
{
    if (!node->since().isEmpty()) {
        Text text;
        if (node->isSharedCommentNode()) {
            const auto &collective = static_cast<const SharedCommentNode *>(node)->collective();
            QString typeStr = typeString(collective.first(), collective.size() > 1);
            text << Atom::ParaLeft << "These " << typeStr << " were introduced in "
                 << formatSince(node) << "." << Atom::ParaRight;
        } else {
            text << Atom::ParaLeft << "This " << typeString(node) << " was introduced in "
                 << formatSince(node) << "." << Atom::ParaRight;
        }
        generateText(text, node, marker);
    }
}

void Generator::generateNoexceptNote(const Node* node, CodeMarker* marker) {
    std::vector<const Node*> nodes;
    if (node->isSharedCommentNode()) {
        auto shared_node = static_cast<const SharedCommentNode*>(node);
        nodes.reserve(shared_node->collective().size());
        nodes.insert(nodes.begin(), shared_node->collective().begin(), shared_node->collective().end());
    } else nodes.push_back(node);

    std::size_t counter{1};
    for (const Node* node : nodes) {
        if (node->isFunction(Genus::CPP)) {
            if (const auto &exception_info = static_cast<const FunctionNode*>(node)->getNoexcept(); exception_info && !(*exception_info).isEmpty()) {
                Text text;
                text << Atom::NoteLeft
                        << (nodes.size() > 1 ? QString::fromStdString(" ("s + std::to_string(counter) + ")"s) : QString::fromStdString("This ") + typeString(node))
                        << " is noexcept when "
                        << Atom(Atom::C, marker->markedUpCode(*exception_info, nullptr, Location()))
                        << " is " << Atom(Atom::C, "true") << "."
                    << Atom::NoteRight;
                generateText(text, node, marker);
            }
        }

        ++counter;
    }
}

void Generator::generateStatus(const Node *node, CodeMarker *marker)
{
    Text text;

    switch (node->status()) {
    case Status::Active:
        // Output the module 'state' description if set.
        if (node->isModule() || node->isQmlModule()) {
            const QString &state = static_cast<const CollectionNode*>(node)->state();
            if (!state.isEmpty()) {
                text << Atom::ParaLeft << "This " << typeString(node) << " is in "
                     << Atom(Atom::FormattingLeft, ATOM_FORMATTING_ITALIC) << state
                     << Atom(Atom::FormattingRight, ATOM_FORMATTING_ITALIC) << " state."
                     << Atom::ParaRight;
                break;
            }
        }
        if (const auto &version = node->deprecatedSince(); !version.isEmpty()) {
            text << Atom::ParaLeft << "This " << typeString(node)
                 << " is scheduled for deprecation in version "
                 << version << "." << Atom::ParaRight;
        }
        break;
    case Status::Preliminary: {
        auto description = Config::instance()
                                   .get(CONFIG_PRELIMINARY + Config::dot + CONFIG_DESCRIPTION)
                                   .asString();
        description.replace('\1'_L1, typeString(node));
        text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD)
             << description
             << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << Atom::ParaRight;
    } break;
    case Status::Deprecated:
        text << Atom::ParaLeft;
        if (node->isAggregate())
            text << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD);
        text << "This " << typeString(node) << " is deprecated";
        if (const QString &version = node->deprecatedSince(); !version.isEmpty()) {
            text << " since ";
            if (node->isQmlNode() && !node->logicalModuleName().isEmpty())
                text << node->logicalModuleName() << " ";
            text << version;
        }

        text << ". We strongly advise against using it in new code.";
        if (node->isAggregate())
            text << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD);
        text << Atom::ParaRight;
        break;
    case Status::Internal:
    case Status::InternalAuto:
        if (node->isPageNode())
            text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD)
                 << "Part of developer documentation for internal use."
                 << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << Atom::ParaRight;
        break;
    default:
        break;
    }
    generateText(text, node, marker);
}

/*!
  Generates an addendum note of type \a type for \a node, using \a marker
  as the code marker.
*/
void Generator::generateAddendum(const Node *node, Addendum type, CodeMarker *marker,
                                 AdmonitionPrefix prefix)
{
    Q_ASSERT(node && !node->name().isEmpty());
    Text text;
    text << Atom(Atom::DivLeft,
                 "class=\"admonition %1\""_L1.arg(prefix == AdmonitionPrefix::Note ? u"note"_s : u"auto"_s));
    text << Atom::ParaLeft;

    switch (prefix) {
    case AdmonitionPrefix::None:
        break;
    case AdmonitionPrefix::Note: {
        text << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD)
             << "Note: " << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD);
        break;
        }
    }

    switch (type) {
    case Invokable:
        text << "This function can be invoked via the meta-object system and from QML. See "
             << Atom(Atom::AutoLink, "Q_INVOKABLE")
             << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
             << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << ".";
        break;
    case PrivateSignal:
        text << "This is a private signal. It can be used in signal connections "
                "but cannot be emitted by the user.";
        break;
    case QmlSignalHandler:
    {
        QString handler(node->name());
        qsizetype prefixLocation = handler.lastIndexOf('.', -2) + 1;
        handler[prefixLocation] = handler[prefixLocation].toTitleCase();
        handler.insert(prefixLocation, QLatin1String("on"));
        text << "The corresponding handler is "
             << Atom(Atom::FormattingLeft, ATOM_FORMATTING_TELETYPE) << handler
             << Atom(Atom::FormattingRight, ATOM_FORMATTING_TELETYPE) << ".";
        break;
    }
    case AssociatedProperties:
    {
        if (!node->isFunction())
            return;
        const auto *fn = static_cast<const FunctionNode *>(node);
        auto nodes = fn->associatedProperties();
        if (nodes.isEmpty())
            return;
        std::sort(nodes.begin(), nodes.end(), Node::nodeNameLessThan);

        // Group properties by their role for more concise output
        QMap<PropertyNode::FunctionRole, QList<const PropertyNode *>> roleGroups;
        for (const auto *n : std::as_const(nodes)) {
            const auto *pn = static_cast<const PropertyNode *>(n);
            if (pn->isInAPI()) {
                PropertyNode::FunctionRole role = pn->role(fn);
                roleGroups[role].append(pn);
            }
        }

        if (roleGroups.isEmpty())
            return;

        // Generate text for each role group in an explicit order
        static constexpr PropertyNode::FunctionRole roleOrder[] = {
             PropertyNode::FunctionRole::Getter,
             PropertyNode::FunctionRole::Setter,
             PropertyNode::FunctionRole::Resetter,
             PropertyNode::FunctionRole::Notifier,
             PropertyNode::FunctionRole::Bindable,
        };
        for (auto role : roleOrder) {
            const auto it = roleGroups.constFind(role);
            if (it == roleGroups.cend())
                continue;

            const auto &properties = it.value();

            QString msg;
            switch (role) {
            case PropertyNode::FunctionRole::Getter:
                msg = u"Getter function"_s;
                break;
            case PropertyNode::FunctionRole::Setter:
                msg = u"Setter function"_s;
                break;
            case PropertyNode::FunctionRole::Resetter:
                msg = u"Resetter function"_s;
                break;
            case PropertyNode::FunctionRole::Notifier:
                msg = u"Notifier signal"_s;
                break;
            case PropertyNode::FunctionRole::Bindable:
                msg = u"Bindable function"_s;
                break;
            default:
                continue;
            }

            if (properties.size() == 1) {
                const auto *pn = properties.first();
                text << msg << u" for property "_s << Atom(Atom::Link, pn->name())
                     << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << pn->name()
                     << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << u". "_s;
            } else {
                text << msg << u" for properties "_s;
                for (qsizetype i = 0; i < properties.size(); ++i) {
                    const auto *pn = properties.at(i);
                    text << Atom(Atom::Link, pn->name())
                         << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << pn->name()
                         << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK)
                         << TextUtils::separator(i, properties.size());
                }
                text << u" "_s;
            }
        }
        break;
    }
    case BindableProperty:
    {
        text << "This property supports "
             << Atom(Atom::Link, "QProperty")
             << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << "QProperty"
             << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
        text << " bindings.";
        break;
    }
    case OverloadNote:
    {
        const auto *func = static_cast<const FunctionNode *>(node);

        // Primary overloads should not display any overload note text
        if (func->isPrimaryOverload())
            return;

        if (func->isSignal() || func->isSlot()) {
            QString functionType = func->isSignal() ? "signal" : "slot";
            const QString &configKey = func->isSignal() ? "overloadedsignalstarget" : "overloadedslotstarget";
            const QString &defaultTarget = func->isSignal() ? "connecting-overloaded-signals" : "connecting-overloaded-slots";
            const QString &linkTarget = Config::instance().get(configKey).asString(defaultTarget);

            text << "This " << functionType << " is overloaded. ";

            QString snippet = generateOverloadSnippet(func);
            if (!snippet.isEmpty()) {
                text << "To connect to this " << functionType << ":\n\n"
                     << Atom(Atom::Code, snippet) << "\n";
            }

            if (!linkTarget.isEmpty()) {
                text << "For more examples and approaches, see "
                     << Atom(Atom::Link, linkTarget)
                     << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
                     << "connecting to overloaded " << functionType << "s"
                     << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << ".";
            }
        } else {
            const auto &args = node->doc().overloadList();
            if (args.first().first.isEmpty()) {
                text << "This is an overloaded function.";
            } else {
                QString target = args.first().first;
                // If the target is not fully qualified and we have a parent class context,
                // attempt to qualify it to improve link resolution
                if (!target.contains("::")) {
                    const auto *parent = node->parent();
                    if (parent && (parent->isClassNode() || parent->isNamespace()))
                        target = parent->name() + "::" + target;
                }
                text << "This function overloads " << Atom(Atom::AutoLink, target) << ".";
            }
        }
        break;
    }
    default:
        return;
    }

    text << Atom::ParaRight
         << Atom::DivRight;
    generateText(text, node, marker);
}

/*!
  Generate the documentation for \a relative. i.e. \a relative
  is the node that represents the entity where a qdoc comment
  was found, and \a text represents the qdoc comment.
 */
bool Generator::generateText(const Text &text, const Node *relative, CodeMarker *marker)
{
    bool result = false;
    if (text.firstAtom() != nullptr) {
        int numAtoms = 0;
        initializeTextOutput();
        generateAtomList(text.firstAtom(), relative, marker, true, numAtoms);
        result = true;
    }
    return result;
}

/*
  The node is an aggregate, typically a class node, which has
  a threadsafeness level. This function checks all the children
  of the node to see if they are exceptions to the node's
  threadsafeness. If there are any exceptions, the exceptions
  are added to the appropriate set (reentrant, threadsafe, and
  nonreentrant, and true is returned. If there are no exceptions,
  the three node lists remain empty and false is returned.
 */
bool Generator::hasExceptions(const Node *node, NodeList &reentrant, NodeList &threadsafe,
                              NodeList &nonreentrant)
{
    bool result = false;
    Node::ThreadSafeness ts = node->threadSafeness();
    const NodeList &children = static_cast<const Aggregate *>(node)->childNodes();
    for (auto child : children) {
        if (!child->isDeprecated()) {
            switch (child->threadSafeness()) {
            case Node::Reentrant:
                reentrant.append(child);
                if (ts == Node::ThreadSafe)
                    result = true;
                break;
            case Node::ThreadSafe:
                threadsafe.append(child);
                if (ts == Node::Reentrant)
                    result = true;
                break;
            case Node::NonReentrant:
                nonreentrant.append(child);
                result = true;
                break;
            default:
                break;
            }
        }
    }
    return result;
}

/*!
    Returns \c true if a trademark symbol should be appended to the
    output as determined by \a atom. Trademarks are tracked via the
    use of the \\tm formatting command.

    Returns true if:

    \list
        \li \a atom is of type Atom::FormattingRight containing
            ATOM_FORMATTING_TRADEMARK, and
        \li The trademarked string is the first appearance on the
            current sub-page.
    \endlist
*/
bool Generator::appendTrademark(const Atom *atom)
{
    if (atom->type() != Atom::FormattingRight)
        return false;
    if (atom->string() != ATOM_FORMATTING_TRADEMARK)
        return false;

    if (atom->count() > 1) {
        if (s_trademarks.contains(atom->string(1)))
            return false;
        s_trademarks << atom->string(1);
    }

    return true;
}

static void startNote(Text &text)
{
    text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD)
         << "Note:" << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << " ";
}

/*!
  Generates text that explains how threadsafe and/or reentrant
  \a node is.
 */
void Generator::generateThreadSafeness(const Node *node, CodeMarker *marker)
{
    Text text, rlink, tlink;
    NodeList reentrant;
    NodeList threadsafe;
    NodeList nonreentrant;
    Node::ThreadSafeness ts = node->threadSafeness();
    bool exceptions = false;

    rlink << Atom(Atom::Link, "reentrant") << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
          << "reentrant" << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);

    tlink << Atom(Atom::Link, "thread-safe") << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
          << "thread-safe" << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);

    switch (ts) {
    case Node::UnspecifiedSafeness:
        break;
    case Node::NonReentrant:
        text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD)
             << "Warning:" << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << " This "
             << typeString(node) << " is not " << rlink << "." << Atom::ParaRight;
        break;
    case Node::Reentrant:
    case Node::ThreadSafe:
        startNote(text);
        if (node->isAggregate()) {
            exceptions = hasExceptions(node, reentrant, threadsafe, nonreentrant);
            text << "All functions in this " << typeString(node) << " are ";
            if (ts == Node::ThreadSafe)
                text << tlink;
            else
                text << rlink;

            if (!exceptions || (ts == Node::Reentrant && !threadsafe.isEmpty()))
                text << ".";
            else
                text << " with the following exceptions:";
        } else {
            text << "This " << typeString(node) << " is ";
            if (ts == Node::ThreadSafe)
                text << tlink;
            else
                text << rlink;
            text << ".";
        }
        text << Atom::ParaRight;
        break;
    default:
        break;
    }
    generateText(text, node, marker);

    if (exceptions) {
        text.clear();
        if (ts == Node::Reentrant) {
            if (!nonreentrant.isEmpty()) {
                startNote(text);
                text << "These functions are not " << rlink << ":" << Atom::ParaRight;
                signatureList(nonreentrant, node, marker);
            }
            if (!threadsafe.isEmpty()) {
                text.clear();
                startNote(text);
                text << "These functions are also " << tlink << ":" << Atom::ParaRight;
                generateText(text, node, marker);
                signatureList(threadsafe, node, marker);
            }
        } else { // thread-safe
            if (!reentrant.isEmpty()) {
                startNote(text);
                text << "These functions are only " << rlink << ":" << Atom::ParaRight;
                signatureList(reentrant, node, marker);
            }
            if (!nonreentrant.isEmpty()) {
                text.clear();
                startNote(text);
                text << "These functions are not " << rlink << ":" << Atom::ParaRight;
                signatureList(nonreentrant, node, marker);
            }
        }
    }
}

/*!
  \internal

  Generates text that describes the comparison category of \a node.
  The CodeMarker \a marker is passed along to generateText().
 */
bool Generator::generateComparisonCategory(const Node *node, CodeMarker *marker)
{
    auto category{node->comparisonCategory()};
    if (category == ComparisonCategory::None)
        return false;

    Text text;
    text << Atom::ParaLeft << "%1 is "_L1.arg(node->plainFullName())
         << Atom(Atom::FormattingLeft, ATOM_FORMATTING_ITALIC)
         << QString::fromStdString(comparisonCategoryAsString(category))
         << ((category == ComparisonCategory::Equality) ? "-"_L1 : "ly "_L1)
         << Atom(Atom::String, "comparable"_L1)
         << Atom(Atom::FormattingRight, ATOM_FORMATTING_ITALIC)
         << "."_L1 << Atom::ParaRight;
    generateText(text, node, marker);
    return true;
}

/*!
    Generates a table of comparison categories for \a node, combining both
    self-comparison (from \\compares) and comparisons with other types
    (from \\compareswith).

    If the node has a comparison category set via \\compares, it appears
    as the first row in the table. Subsequent rows come from \\compareswith
    entries.

    The Description column is only included if at least one \\compareswith
    entry has descriptive content.

    Returns \c true if text was generated, \c false otherwise.
 */
bool Generator::generateComparisonTable(const Node *node)
{
    Q_ASSERT(node);

    const auto selfCategory = node->comparisonCategory();
    const auto *map = node->doc().comparesWithMap();

    const bool hasSelfComparison = (selfCategory != ComparisonCategory::None);
    const bool hasComparesWithEntries = (map && !map->isEmpty());

    if (!hasSelfComparison && !hasComparesWithEntries)
        return false;

    bool hasDescriptions = false;
    if (hasComparesWithEntries) {
        for (const auto &description : *map) {
            if (description.firstAtom()->next() != description.lastAtom()) {
                hasDescriptions = true;
                break;
            }
        }
    }

    Text text;

    text << Atom::ParaLeft
         << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD)
         << "%1 Comparisons"_L1.arg(node->plainFullName())
         << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD)
         << Atom::ParaRight;

    text << Atom(Atom::TableLeft, "generic"_L1);

    text << Atom::TableHeaderLeft
         << Atom::TableItemLeft << "Category"_L1 << Atom::TableItemRight
         << Atom::TableItemLeft << "Comparable Types"_L1 << Atom::TableItemRight;
    if (hasDescriptions)
        text << Atom::TableItemLeft << "Description"_L1 << Atom::TableItemRight;
    text << Atom::TableHeaderRight;

    // First row: self-comparison from \compares
    if (hasSelfComparison) {
        const QString &category = QString::fromStdString(comparisonCategoryAsString(selfCategory));

        text << Atom::TableRowLeft;
        text << Atom::TableItemLeft << category << Atom::TableItemRight;
        text << Atom::TableItemLeft
             << Atom(Atom::String, node->plainFullName()) << Atom::TableItemRight;
        if (hasDescriptions)
            text << Atom::TableItemLeft << Atom::TableItemRight;
        text << Atom::TableRowRight;
    }

    // Subsequent rows: \compareswith entries
    if (hasComparesWithEntries) {
        for (auto [key, description] : map->asKeyValueRange()) {
            const QString &category = QString::fromStdString(comparisonCategoryAsString(key));

            text << Atom::TableRowLeft;

            text << Atom::TableItemLeft << category << Atom::TableItemRight;

            text << Atom::TableItemLeft;
            const QStringList types{description.firstAtom()->string().split(';'_L1)};
            for (const auto &name : types)
                text << Atom(Atom::AutoLink, name)
                     << TextUtils::separator(types.indexOf(name), types.size());
            text << Atom::TableItemRight;

            if (hasDescriptions) {
                text << Atom::TableItemLeft;
                if (description.firstAtom()->next() != description.lastAtom())
                    text << Text::subText(description.firstAtom()->next(), description.lastAtom());
                text << Atom::TableItemRight;
            }

            text << Atom::TableRowRight;
        }
    }

    text << Atom::TableRight;

    generateText(text, node, nullptr);
    return !text.isEmpty();
}

/*!
  Traverses the database recursively to generate all the documentation.
 */
void Generator::generateDocs()
{
    s_currentGenerator = this;
    generateDocumentation(m_qdb->primaryTreeRoot());
}

Generator *Generator::generatorForFormat(const QString &format)
{
    // First, check the OutputProducerRegistry for producers.
    // This supports both Generator-based producers (which register themselves)
    // and future non-Generator OutputProducer implementations.
    if (auto *producer = OutputProducerRegistry::instance().producerForFormat(format)) {
        // TODO: All registered producers are Generators, but this will
        // change as we migrate to OutputProducer-based implementations.
        if (auto *gen = dynamic_cast<Generator *>(producer))
            return gen;
    }

    // Fallback: Check the legacy s_generators list for unregistered generators.
    // This should not normally be reached, but provides backward compatibility.
    for (const auto &generator : std::as_const(s_generators)) {
        if (generator->format() == format)
            return generator;
    }
    return nullptr;
}

QString Generator::indent(int level, const QString &markedCode)
{
    if (level == 0)
        return markedCode;

    QString t;
    int column = 0;

    int i = 0;
    while (i < markedCode.size()) {
        if (markedCode.at(i) == QLatin1Char('\n')) {
            column = 0;
        } else {
            if (column == 0) {
                for (int j = 0; j < level; j++)
                    t += QLatin1Char(' ');
            }
            column++;
        }
        t += markedCode.at(i++);
    }
    return t;
}

void Generator::initialize()
{
    Config &config = Config::instance();
    s_outputFormats = config.getOutputFormats();
    s_redirectDocumentationToDevNull = config.get(CONFIG_REDIRECTDOCUMENTATIONTODEVNULL).asBool();

    for (auto &g : s_generators) {
        if (s_outputFormats.contains(g->format())) {
            s_currentGenerator = g;
            OutputProducerRegistry::instance().registerProducer(g);
            g->initializeGenerator();
        }
    }

    const auto &configFormatting = config.subVars(CONFIG_FORMATTING);
    for (const auto &n : configFormatting) {
        QString formattingDotName = CONFIG_FORMATTING + Config::dot + n;
        const auto &formattingDotNames = config.subVars(formattingDotName);
        for (const auto &f : formattingDotNames) {
            const auto &configVar = config.get(formattingDotName + Config::dot + f);
            QString def{configVar.asString()};
            if (!def.isEmpty()) {
                int numParams = Config::numParams(def);
                int numOccs = def.count("\1");
                if (numParams != 1) {
                    configVar.location().warning(QStringLiteral("Formatting '%1' must "
                                                                "have exactly one "
                                                                "parameter (found %2)")
                                                                .arg(n, numParams));
                } else if (numOccs > 1) {
                    configVar.location().fatal(QStringLiteral("Formatting '%1' must "
                                             "contain exactly one "
                                             "occurrence of '\\1' "
                                             "(found %2)")
                                            .arg(n, numOccs));
                } else {
                    int paramPos = def.indexOf("\1");
                    s_fmtLeftMaps[f].insert(n, def.left(paramPos));
                    s_fmtRightMaps[f].insert(n, def.mid(paramPos + 1));
                }
            }
        }
    }

    s_project = config.get(CONFIG_PROJECT).asString();
    s_outDir = config.getOutputDir();
    s_outSubdir = s_outDir.mid(s_outDir.lastIndexOf('/') + 1);

    s_outputPrefixes.clear();
    QStringList items{config.get(CONFIG_OUTPUTPREFIXES).asStringList()};
    if (!items.isEmpty()) {
        for (const auto &prefix : items)
            s_outputPrefixes[prefix] =
                    config.get(CONFIG_OUTPUTPREFIXES + Config::dot + prefix).asString();
    }
    if (!items.contains(u"QML"_s))
        s_outputPrefixes[u"QML"_s] = u"qml-"_s;

    s_outputSuffixes.clear();
    for (const auto &suffix : config.get(CONFIG_OUTPUTSUFFIXES).asStringList())
        s_outputSuffixes[suffix] = config.get(CONFIG_OUTPUTSUFFIXES
                                              + Config::dot + suffix).asString();

    s_noLinkErrors = config.get(CONFIG_NOLINKERRORS).asBool();
    s_autolinkErrors = config.get(CONFIG_AUTOLINKERRORS).asBool();
}

/*!
  Creates template-specific subdirs (e.g. /styles and /scripts for HTML)
  and copies the files to them.
  */
void Generator::copyTemplateFiles(const QString &configVar, const QString &subDir)
{
    // TODO: [resolving-files-unlinked-to-doc]
    // This is another case of resolving files, albeit it doesn't use Doc::resolveFile.
    // While it may be left out of a first iteration of the file
    // resolution logic, it should later be integrated into it.
    // This should come naturally when the output directory logic is
    // extracted and copying a file should require a validated
    // intermediate format.
    // Do note that what is done here is a bit different from the
    // resolve file routine that is done for other user-given paths.
    // Thas is, the paths will always be absolute and not relative as
    // they are resolved from the configuration.
    // Ideally, this could be solved in the configuration already,
    // together with the other configuration resolution processes that
    // do not abide by the same constraints that, for example, snippet
    // resolution uses.
    Config &config = Config::instance();
    QStringList files = config.getCanonicalPathList(configVar, Config::Validate);
    const auto &loc = config.get(configVar).location();
    if (!files.isEmpty()) {
        // TODO: [uncentralized-output-directory-structure]
        // OutputDirectory provides the centralized system for managing output
        // directory structure in Generator base class methods. However, the
        // format-specific generators (HtmlGenerator, DocBookGenerator,
        // WebXMLGenerator) still manually construct image paths using string
        // concatenation and direct Config::copyFile() calls. These should be
        // refactored to use OutputDirectory for consistency and security.

        const OutputDirectory outDir =
                OutputDirectory::ensure(s_outDir, loc);

        const OutputDirectory templateDir =
                outDir.ensureSubdir(subDir, loc);

        for (const auto &file : files) {
            if (!file.isEmpty()) {
                const QFileInfo fi(file);
                Config::copyFile(loc, fi.absoluteFilePath(), fi.fileName(), templateDir.path());
            }
        }
    }
}

/*!
    Reads format-specific variables from config, sets output
    (sub)directories, creates them on the filesystem and copies the
    template-specific files.
 */
void Generator::initializeFormat()
{
    Config &config = Config::instance();
    s_outFileNames.clear();
    s_useOutputSubdirs = true;
    if (config.get(format() + Config::dot + "nosubdirs").asBool())
        resetUseOutputSubdirs();

    if (s_outputFormats.isEmpty())
        return;
    if (s_redirectDocumentationToDevNull)
        return;

    s_outDir = config.getOutputDir(format());
    if (s_outDir.isEmpty()) {
        Location().fatal(QStringLiteral("No output directory specified in "
                                        "configuration file or on the command line"));
    } else {
        s_outSubdir = s_outDir.mid(s_outDir.lastIndexOf('/') + 1);
    }

    // Ensure output directory exists before proceeding
    const OutputDirectory outputDir =
            OutputDirectory::ensure(s_outDir, Location());

    // Check if the directory is empty when required
    if (!config.generating() && Generator::useOutputSubdirs()) {
        if (!outputDir.toQDir().isEmpty())
            Location().error("Output directory '%1' exists but is not empty"_L1.arg(s_outDir));
    }

    // Output directory exists, which is enough for prepare phase.
    if (config.preparing())
        return;

    auto imagesDir = config.get(CONFIG_IMAGESOUTPUTDIR).asString(u"images"_s);
    // Ensure images subdirectory exists
    [[maybe_unused]] const OutputDirectory imagesOutputDir =
            outputDir.ensureSubdir(imagesDir, Location());
    s_imagesOutDir = std::move(imagesDir);

    copyTemplateFiles(format() + Config::dot + CONFIG_STYLESHEETS, "style");
    copyTemplateFiles(format() + Config::dot + CONFIG_SCRIPTS, "scripts");
    copyTemplateFiles(format() + Config::dot + CONFIG_EXTRAIMAGES, "images");

    // Use a format-specific .quotinginformation if defined, otherwise a global value
    if (config.subVars(format()).contains(CONFIG_QUOTINGINFORMATION))
        m_quoting = config.get(format() + Config::dot + CONFIG_QUOTINGINFORMATION).asBool();
    else
        m_quoting = config.get(CONFIG_QUOTINGINFORMATION).asBool();
}

/*!
  No-op base implementation. Subclasses may override to perform
  generator-specific initialization.
 */
void Generator::initializeGenerator()
{
    // Default implementation does nothing
}

bool Generator::matchAhead(const Atom *atom, Atom::AtomType expectedAtomType)
{
    return atom->next() && atom->next()->type() == expectedAtomType;
}

/*!
  Used for writing to the current output stream. Returns a
  reference to the current output stream, which is then used
  with the \c {<<} operator for writing.
 */
QTextStream &Generator::out()
{
    return *outStreamStack.top();
}

QString Generator::outFileName()
{
    return QFileInfo(static_cast<QFile *>(out().device())->fileName()).fileName();
}

QString Generator::outputPrefix(const Node *node)
{
    // Omit prefix for module pages
    if (node->isPageNode() && !node->isCollectionNode()) {
        switch (node->genus()) {
        case Genus::QML:
            return s_outputPrefixes[u"QML"_s];
        case Genus::CPP:
            return s_outputPrefixes[u"CPP"_s];
        default:
            break;
        }
    }
    return QString();
}

QString Generator::outputSuffix(const Node *node)
{
    if (node->isPageNode()) {
        switch (node->genus()) {
        case Genus::QML:
            return s_outputSuffixes[u"QML"_s];
        case Genus::CPP:
            return s_outputSuffixes[u"CPP"_s];
        default:
            break;
        }
    }

    return QString();
}

bool Generator::parseArg(const QString &src, const QString &tag, int *pos, int n,
                         QStringView *contents, QStringView *par1)
{
#define SKIP_CHAR(c)                                                                               \
    if (i >= n || src[i] != c)                                                                     \
        return false;                                                                              \
    ++i;

#define SKIP_SPACE                                                                                 \
    while (i < n && src[i] == ' ')                                                                 \
        ++i;

    qsizetype i = *pos;
    qsizetype j {};

    // assume "<@" has been parsed outside
    // SKIP_CHAR('<');
    // SKIP_CHAR('@');

    if (tag != QStringView(src).mid(i, tag.size())) {
        return false;
    }

    // skip tag
    i += tag.size();

    // parse stuff like:  linkTag("(<@link node=\"([^\"]+)\">).*(</@link>)");
    if (par1) {
        SKIP_SPACE;
        // read parameter name
        j = i;
        while (i < n && src[i].isLetter())
            ++i;
        if (src[i] == '=') {
            SKIP_CHAR('=');
            SKIP_CHAR('"');
            // skip parameter name
            j = i;
            while (i < n && src[i] != '"')
                ++i;
            *par1 = QStringView(src).mid(j, i - j);
            SKIP_CHAR('"');
            SKIP_SPACE;
        }
    }
    SKIP_SPACE;
    SKIP_CHAR('>');

    // find contents up to closing "</@tag>
    j = i;
    for (; true; ++i) {
        if (i + 4 + tag.size() > n)
            return false;
        if (src[i] != '<')
            continue;
        if (src[i + 1] != '/')
            continue;
        if (src[i + 2] != '@')
            continue;
        if (tag != QStringView(src).mid(i + 3, tag.size()))
            continue;
        if (src[i + 3 + tag.size()] != '>')
            continue;
        break;
    }

    *contents = QStringView(src).mid(j, i - j);

    i += tag.size() + 4;

    *pos = i;
    return true;
#undef SKIP_CHAR
#undef SKIP_SPACE
}

QString Generator::plainCode(const QString &markedCode)
{
    QString t = markedCode;
    t.replace(tag, QString());
    t.replace(quot, QLatin1String("\""));
    t.replace(gt, QLatin1String(">"));
    t.replace(lt, QLatin1String("<"));
    t.replace(amp, QLatin1String("&"));
    return t;
}

int Generator::skipAtoms(const Atom *atom, Atom::AtomType type) const
{
    int skipAhead = 0;
    atom = atom->next();
    while (atom && atom->type() != type) {
        skipAhead++;
        atom = atom->next();
    }
    return skipAhead;
}

/*!
  Resets the variables used during text output.
 */
void Generator::initializeTextOutput()
{
    m_inLink = false;
    m_inContents = false;
    m_inSectionHeading = false;
    m_inTableHeader = false;
    m_numTableRows = 0;
    m_threeColumnEnumValueTable = true;
    m_link.clear();
    m_sectionNumber.clear();
}

void Generator::supplementAlsoList(const Node *node, QList<Text> &alsoList)
{
    if (node->isFunction() && !node->isMacro()) {
        const auto fn = static_cast<const FunctionNode *>(node);
        if (fn->overloadNumber() == 0) {
            QString alternateName;
            const FunctionNode *alternateFunc = nullptr;

            if (fn->name().startsWith("set") && fn->name().size() >= 4) {
                alternateName = fn->name()[3].toLower();
                alternateName += fn->name().mid(4);
                alternateFunc = fn->parent()->findFunctionChild(alternateName, QString());

                if (!alternateFunc) {
                    alternateName = "is" + fn->name().mid(3);
                    alternateFunc = fn->parent()->findFunctionChild(alternateName, QString());
                    if (!alternateFunc) {
                        alternateName = "has" + fn->name().mid(3);
                        alternateFunc = fn->parent()->findFunctionChild(alternateName, QString());
                    }
                }
            } else if (!fn->name().isEmpty()) {
                alternateName = "set";
                alternateName += fn->name()[0].toUpper();
                alternateName += fn->name().mid(1);
                alternateFunc = fn->parent()->findFunctionChild(alternateName, QString());
            }

            if (alternateFunc && alternateFunc->access() != Access::Private) {
                int i;
                for (i = 0; i < alsoList.size(); ++i) {
                    if (alsoList.at(i).toString().contains(alternateName))
                        break;
                }

                if (i == alsoList.size()) {
                    if (alternateFunc->isDeprecated() && !fn->isDeprecated())
                        return;
                    alternateName += "()";

                    Text also;
                    also << Atom(Atom::Link, alternateName)
                         << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << alternateName
                         << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
                    alsoList.prepend(also);
                }
            }
        }
    }
}

void Generator::generateEnumValuesForQmlReference(const Node *node, CodeMarker *marker)
{
    const NativeEnum *nativeEnum{nullptr};
    if (auto *ne_if = dynamic_cast<const NativeEnumInterface *>(node))
        nativeEnum = ne_if->nativeEnum();
    else
        return;

    if (!nativeEnum->enumNode())
        return;

    // Retrieve atoms from C++ enum \value list
    const auto body{nativeEnum->enumNode()->doc().body()};
    const auto *start{body.firstAtom()};
    Text text;

    while ((start = start->find(Atom::ListLeft, ATOM_LIST_VALUE))) {
        const auto end = start->find(Atom::ListRight, ATOM_LIST_VALUE);
        // Skip subsequent ListLeft atoms, collating multiple lists into one
        text << body.subText(text.isEmpty() ? start : start->next(), end);
        start = end;
    }
    if (text.isEmpty())
        return;

    text << Atom(Atom::ListRight, ATOM_LIST_VALUE);
    if (marker)
        generateText(text, node, marker);
    else
        generateText(text, node);
}

void Generator::terminate()
{
    for (const auto &generator : std::as_const(s_generators)) {
        if (s_outputFormats.contains(generator->format())) {
            OutputProducerRegistry::instance().unregisterProducer(generator);
            generator->terminateGenerator();
        }
    }

    // REMARK: Generators currently, due to recent changes and the
    // transitive nature of the current codebase, receive some of
    // their dependencies in the constructor and some of them in their
    // initialize-terminate lifetime.
    // This means that generators need to be constructed and
    // destructed between usages such that if multiple usages are
    // required, the generators present in the list will have been
    // destroyed by then such that accessing them would be an error.
    // The current codebase calls initialize and the correspective
    // terminate with the same scope as the lifetime of the
    // generators.
    // Then, clearing the list ensures that, if another generator
    // execution is needed, the stale generators will not be removed
    // as to be replaced by newly constructed ones.
    // Do note that it is not clear that this will happen for any call
    // in Qt's documentation and this should work only because of the
    // form of the current codebase and the scoping of the
    // initialize-terminate calls. As such, this should be considered
    // a patchwork that may or may not be doing anything and that may
    // break due to changes in other parts of the codebase.
    //
    // This is still to be considered temporary as the whole
    // initialize-terminate idiom must be removed from the codebase.
    s_generators.clear();

    s_fmtLeftMaps.clear();
    s_fmtRightMaps.clear();
    s_outDir.clear();
    s_imagesOutDir.clear();
}

void Generator::terminateGenerator() {}

/*!
  Trims trailing whitespace off the \a string and returns
  the trimmed string.
 */
QString Generator::trimmedTrailing(const QString &string, const QString &prefix,
                                   const QString &suffix)
{
    QString trimmed = string;
    while (trimmed.size() > 0 && trimmed[trimmed.size() - 1].isSpace())
        trimmed.truncate(trimmed.size() - 1);

    trimmed.append(suffix);
    trimmed.prepend(prefix);
    return trimmed;
}

QString Generator::typeString(const Node *node, bool plural)
{
    switch (node->nodeType()) {
    case NodeType::Namespace:
        return plural ? "namespaces"_L1 : "namespace"_L1;
    case NodeType::Class:
        return plural ? "classes"_L1 : "class"_L1;
    case NodeType::Struct:
        return plural ? "structs"_L1 : "struct"_L1;
    case NodeType::Union:
        return plural ? "unions"_L1 : "union"_L1;
    case NodeType::QmlType:
    case NodeType::QmlValueType:
        return plural ? "types"_L1 : "type"_L1;
    case NodeType::Page:
        return "documentation"_L1;
    case NodeType::Enum:
        return plural ? "enums"_L1 : "enum"_L1;
    case NodeType::Typedef:
    case NodeType::TypeAlias:
        return plural ? "typedefs"_L1 : "typedef"_L1;
    case NodeType::Function: {
        const auto fn = static_cast<const FunctionNode *>(node);
        switch (fn->metaness()) {
        case Metaness::QmlSignal:
            return plural ? "signals"_L1 : "signal"_L1;
        case Metaness::QmlSignalHandler:
            return plural ? "signal handlers"_L1 : "signal handler"_L1;
        case Metaness::QmlMethod:
            return plural ? "methods"_L1 : "method"_L1;
        case Metaness::MacroWithParams:
        case Metaness::MacroWithoutParams:
            return plural ? "macros"_L1 : "macro"_L1;
        default:
            break;
        }
        return plural ? "functions"_L1 : "function"_L1;
    }
    case NodeType::Property:
    case NodeType::QmlEnum:
        return plural ? "enumerations"_L1 : "enumeration"_L1;
    case NodeType::QmlProperty:
        return plural ? "properties"_L1 : "property"_L1;
    case NodeType::Module:
    case NodeType::QmlModule:
        return plural ? "modules"_L1 : "module"_L1;
    case NodeType::Variable:
        return plural ? "variables"_L1 : "variable"_L1;
    case NodeType::SharedComment: {
        const auto *shared = static_cast<const SharedCommentNode *>(node);
        if (shared->isPropertyGroup())
            return plural ? "property groups"_L1 : "property group"_L1;
        const auto &collective = shared->collective();
        return collective.first()->nodeTypeString();
    }
    default:
        return "documentation"_L1;
    }
}

void Generator::unknownAtom(const Atom *atom)
{
    Location::internalError(QStringLiteral("unknown atom type '%1' in %2 generator")
                                    .arg(atom->typeString(), format()));
}

/*!
 * Generate the CMake requisite for the node \a cn, i.e. the the find_package and target_link_libraries
 * calls to use it.
 *
 * If only cmakepackage is set it will look like
 *
 * \badcode
 * find_package(Foo REQUIRED)
 * target_link_libraries(mytarget PRIVATE Foo:Foo)
 * \endcode
 *
 * If no cmakepackage is set Qt6 is assumed.
 *
 * If cmakecomponent is set it will look like
 *
 * \badcode
 * find_package(Qt6 REQUIRED COMPONENTS Bar)
 * target_link_libraries(mytarget PRIVATE Qt6::Bar)
 * \endcode
 *
 * If cmaketargetitem is set the item in target_link_libraries will be set accordingly
 *
 * \badcode
 * find_package(Qt6 REQUIRED COMPONENTS Bar)
 * target_link_libraries(mytarget PRIVATE My::Target)
 * \endcode
 *
 * Returns a pair consisting of the find package line and link libraries line.
 *
 * If no sensible requisite can be created (i.e. both cmakecomponent and cmakepackage are unset)
 * \c std::nullopt is returned.
 */
std::optional<std::pair<QString, QString>> Generator::cmakeRequisite(const CollectionNode *cn)
{
    if (!cn || (cn->cmakeComponent().isEmpty() && cn->cmakePackage().isEmpty())) {
        return {};
    }

    const QString package =
        cn->cmakePackage().isEmpty() ? "Qt" + QString::number(QT_VERSION_MAJOR) : cn->cmakePackage();

    QString findPackageText;
    if (cn->cmakeComponent().isEmpty()) {
        findPackageText = "find_package(" + package + " REQUIRED)";
    } else {
        findPackageText = "find_package(" + package + " REQUIRED COMPONENTS " + cn->cmakeComponent() + ")";
    }

    QString targetText;
    if (cn->cmakeTargetItem().isEmpty()) {
        if (cn->cmakeComponent().isEmpty()) {
            targetText = package + "::" + package;
        } else {
            targetText = package + "::" + cn->cmakeComponent();
        }
    } else {
        targetText = cn->cmakeTargetItem();
    }

    const QString targetLinkLibrariesText = "target_link_libraries(mytarget PRIVATE " + targetText + ")";
    const QStringList cmakeInfo { findPackageText, targetLinkLibrariesText };

    return std::make_pair(findPackageText, targetLinkLibrariesText);
}

/*!
    \brief Adds a formatted link to the specified \a text stream.

    This function creates a sequence of Atom objects that together form a link
    and appends them to the \a text. The \a nodeRef parameter specifies the
    target of the link (typically obtained via stringForNode()), and \a linkText
    specifies the visible text for the link.

    \sa Atom, stringForNode()
*/
void Generator::addNodeLink(Text &text, const QString &nodeRef, const QString &linkText) {
    text << Atom(Atom::LinkNode, nodeRef)
         << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
         << Atom(Atom::String, linkText)
         << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
}

/*!
    \overload

    This convenience overload automatically obtains the node reference string
    using stringForNode(). If \a linkText is empty, the node's name is used as
    the link text; otherwise, the specified \a linkText is used.

    \sa stringForNode()
*/
void Generator::addNodeLink(Text &text, const INode *node, const QString &linkText) {
    addNodeLink(
        text,
        Utilities::stringForNode(node),
        linkText.isEmpty() ? node->name() : linkText
    );
}

/*!
  Generates a contextual code snippet for connecting to an overloaded signal or
  slot. Returns an empty string if the function is not a signal or slot.

  For signals, the snippet shows the signal in the second argument position of
  connect(). For slots, the snippet shows the slot in the fourth argument
  position (receiver side).
*/
QString Generator::generateOverloadSnippet(const FunctionNode *func)
{
    if (!func || (!func->isSignal() && !func->isSlot()))
        return QString();

    QString className = func->parent()->name();
    QString functionName = func->name();
    QString typeList = func->parameters().generateTypeList();
    QString typeAndNameList = func->parameters().generateTypeAndNameList();
    QString nameList = func->parameters().generateNameList();
    QString objectName = generateObjectName(className);

    QString snippet;

    if (func->isSignal()) {
        snippet = QString(
            "// Connect using qOverload:\n"
            "connect(%1, qOverload<%2>(&%3::%4),\n"
            "        receiver, &ReceiverClass::slot);\n\n"
            "// Or using a lambda:\n"
            "connect(%1, qOverload<%2>(&%3::%4),\n"
            "        this, [](%5) { /* handle %4 */ });")
            .arg(objectName, typeList, className, functionName, typeAndNameList);
    } else {
        snippet = QString(
            "// Connect using qOverload:\n"
            "connect(sender, &SenderClass::signal,\n"
            "        %1, qOverload<%2>(&%3::%4));\n\n"
            "// Or using a lambda as wrapper:\n"
            "connect(sender, &SenderClass::signal,\n"
            "        %1, [receiver = %1](%5) { receiver->%4(%6); });")
            .arg(objectName, typeList, className, functionName, typeAndNameList, nameList);
    }

    return snippet;
}

/*!
  Generates an appropriate object name for code snippets based on the class name.
  Converts class names like "QComboBox" to "comboBox".
*/
QString Generator::generateObjectName(const QString &className)
{
    QString name = className;

    if (name.startsWith('Q') && name.length() > 1)
        name.remove(0, 1);

    if (!name.isEmpty())
        name[0] = name[0].toLower();

    return name;
}

QT_END_NAMESPACE
