summaryrefslogtreecommitdiffstats
path: root/src/qdoc/qdoc/src/qdoc/htmlgenerator.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/qdoc/qdoc/src/qdoc/htmlgenerator.cpp')
-rw-r--r--src/qdoc/qdoc/src/qdoc/htmlgenerator.cpp3716
1 files changed, 3716 insertions, 0 deletions
diff --git a/src/qdoc/qdoc/src/qdoc/htmlgenerator.cpp b/src/qdoc/qdoc/src/qdoc/htmlgenerator.cpp
new file mode 100644
index 000000000..67672f0de
--- /dev/null
+++ b/src/qdoc/qdoc/src/qdoc/htmlgenerator.cpp
@@ -0,0 +1,3716 @@
+// 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 "htmlgenerator.h"
+
+#include "access.h"
+#include "aggregate.h"
+#include "classnode.h"
+#include "collectionnode.h"
+#include "config.h"
+#include "codemarker.h"
+#include "codeparser.h"
+#include "enumnode.h"
+#include "functionnode.h"
+#include "helpprojectwriter.h"
+#include "manifestwriter.h"
+#include "node.h"
+#include "propertynode.h"
+#include "qdocdatabase.h"
+#include "qmlpropertynode.h"
+#include "sharedcommentnode.h"
+#include "tagfilewriter.h"
+#include "tree.h"
+#include "quoter.h"
+#include "utilities.h"
+
+#include <QtCore/qlist.h>
+#include <QtCore/qmap.h>
+#include <QtCore/quuid.h>
+#include <QtCore/qversionnumber.h>
+#include <QtCore/qregularexpression.h>
+
+#include <cctype>
+#include <deque>
+#include <string>
+
+QT_BEGIN_NAMESPACE
+
+using namespace Qt::StringLiterals;
+
+bool HtmlGenerator::s_inUnorderedList { false };
+
+HtmlGenerator::HtmlGenerator(FileResolver& file_resolver) : XmlGenerator(file_resolver) {}
+
+static void addLink(const QString &linkTarget, QStringView nestedStuff, QString *res)
+{
+ if (!linkTarget.isEmpty()) {
+ *res += QLatin1String("<a href=\"");
+ *res += linkTarget;
+ *res += QLatin1String("\" translate=\"no\">");
+ *res += nestedStuff;
+ *res += QLatin1String("</a>");
+ } else {
+ *res += nestedStuff;
+ }
+}
+
+/*!
+ \internal
+ Convenience method that starts an unordered list if not in one.
+ */
+inline void HtmlGenerator::openUnorderedList()
+{
+ if (!s_inUnorderedList) {
+ out() << "<ul>\n";
+ s_inUnorderedList = true;
+ }
+}
+
+/*!
+ \internal
+ Convenience method that closes an unordered list if in one.
+ */
+inline void HtmlGenerator::closeUnorderedList()
+{
+ if (s_inUnorderedList) {
+ out() << "</ul>\n";
+ s_inUnorderedList = false;
+ }
+}
+
+/*!
+ Destroys the HTML output generator. Deletes the singleton
+ instance of HelpProjectWriter and the ManifestWriter instance.
+ */
+HtmlGenerator::~HtmlGenerator()
+{
+ if (m_helpProjectWriter) {
+ delete m_helpProjectWriter;
+ m_helpProjectWriter = nullptr;
+ }
+
+ if (m_manifestWriter) {
+ delete m_manifestWriter;
+ m_manifestWriter = nullptr;
+ }
+}
+
+/*!
+ Initializes the HTML output generator's data structures
+ from the configuration (Config) singleton.
+ */
+void HtmlGenerator::initializeGenerator()
+{
+ static const struct
+ {
+ const char *key;
+ const char *left;
+ const char *right;
+ } defaults[] = { { ATOM_FORMATTING_BOLD, "<b>", "</b>" },
+ { ATOM_FORMATTING_INDEX, "<!--", "-->" },
+ { ATOM_FORMATTING_ITALIC, "<i>", "</i>" },
+ { ATOM_FORMATTING_PARAMETER, "<i translate=\"no\">", "</i>" },
+ { ATOM_FORMATTING_SUBSCRIPT, "<sub>", "</sub>" },
+ { ATOM_FORMATTING_SUPERSCRIPT, "<sup>", "</sup>" },
+ { ATOM_FORMATTING_TELETYPE, "<code translate=\"no\">",
+ "</code>" }, // <tt> tag is not supported in HTML5
+ { ATOM_FORMATTING_TRADEMARK, "", "&#8482;" },
+ { ATOM_FORMATTING_UICONTROL, "<b translate=\"no\">", "</b>" },
+ { ATOM_FORMATTING_UNDERLINE, "<u>", "</u>" },
+ { nullptr, nullptr, nullptr } };
+
+ Generator::initializeGenerator();
+ config = &Config::instance();
+
+ /*
+ The formatting maps are owned by Generator. They are cleared in
+ Generator::terminate().
+ */
+ for (int i = 0; defaults[i].key; ++i) {
+ formattingLeftMap().insert(QLatin1String(defaults[i].key), QLatin1String(defaults[i].left));
+ formattingRightMap().insert(QLatin1String(defaults[i].key),
+ QLatin1String(defaults[i].right));
+ }
+
+ QString formatDot{HtmlGenerator::format() + Config::dot};
+ m_endHeader = config->get(formatDot + CONFIG_ENDHEADER).asString();
+ m_postHeader = config->get(formatDot + HTMLGENERATOR_POSTHEADER).asString();
+ m_postPostHeader = config->get(formatDot + HTMLGENERATOR_POSTPOSTHEADER).asString();
+ m_prologue = config->get(formatDot + HTMLGENERATOR_PROLOGUE).asString();
+
+ m_footer = config->get(formatDot + HTMLGENERATOR_FOOTER).asString();
+ m_address = config->get(formatDot + HTMLGENERATOR_ADDRESS).asString();
+ m_noNavigationBar = config->get(formatDot + HTMLGENERATOR_NONAVIGATIONBAR).asBool();
+ m_navigationSeparator = config->get(formatDot + HTMLGENERATOR_NAVIGATIONSEPARATOR).asString();
+ tocDepth = config->get(formatDot + HTMLGENERATOR_TOCDEPTH).asInt();
+
+ m_project = config->get(CONFIG_PROJECT).asString();
+ m_projectDescription = config->get(CONFIG_DESCRIPTION)
+ .asString(m_project + QLatin1String(" Reference Documentation"));
+
+ m_projectUrl = config->get(CONFIG_URL).asString();
+ tagFile_ = config->get(CONFIG_TAGFILE).asString();
+ naturalLanguage = config->get(CONFIG_NATURALLANGUAGE).asString(QLatin1String("en"));
+
+ m_codeIndent = config->get(CONFIG_CODEINDENT).asInt();
+ m_codePrefix = config->get(CONFIG_CODEPREFIX).asString();
+ m_codeSuffix = config->get(CONFIG_CODESUFFIX).asString();
+
+ /*
+ The help file write should be allocated once and only once
+ per qdoc execution.
+ */
+ if (m_helpProjectWriter)
+ m_helpProjectWriter->reset(m_project.toLower() + ".qhp", this);
+ else
+ m_helpProjectWriter = new HelpProjectWriter(m_project.toLower() + ".qhp", this);
+
+ if (!m_manifestWriter)
+ m_manifestWriter = new ManifestWriter();
+
+ // Documentation template handling
+ m_headerScripts = config->get(formatDot + CONFIG_HEADERSCRIPTS).asString();
+ m_headerStyles = config->get(formatDot + CONFIG_HEADERSTYLES).asString();
+
+ // Retrieve the config for the navigation bar
+ m_homepage = config->get(CONFIG_NAVIGATION
+ + Config::dot + CONFIG_HOMEPAGE).asString();
+
+ m_hometitle = config->get(CONFIG_NAVIGATION
+ + Config::dot + CONFIG_HOMETITLE)
+ .asString(m_homepage);
+
+ m_landingpage = config->get(CONFIG_NAVIGATION
+ + Config::dot + CONFIG_LANDINGPAGE).asString();
+
+ m_landingtitle = config->get(CONFIG_NAVIGATION
+ + Config::dot + CONFIG_LANDINGTITLE)
+ .asString(m_landingpage);
+
+ m_cppclassespage = config->get(CONFIG_NAVIGATION
+ + Config::dot + CONFIG_CPPCLASSESPAGE).asString();
+
+ m_cppclassestitle = config->get(CONFIG_NAVIGATION
+ + Config::dot + CONFIG_CPPCLASSESTITLE)
+ .asString(QLatin1String("C++ Classes"));
+
+ m_qmltypespage = config->get(CONFIG_NAVIGATION
+ + Config::dot + CONFIG_QMLTYPESPAGE).asString();
+
+ m_qmltypestitle = config->get(CONFIG_NAVIGATION
+ + Config::dot + CONFIG_QMLTYPESTITLE)
+ .asString(QLatin1String("QML Types"));
+
+ m_trademarkspage = config->get(CONFIG_NAVIGATION
+ + Config::dot + CONFIG_TRADEMARKSPAGE).asString();
+
+ m_buildversion = config->get(CONFIG_BUILDVERSION).asString();
+}
+
+/*!
+ Gracefully terminates the HTML output generator.
+ */
+void HtmlGenerator::terminateGenerator()
+{
+ Generator::terminateGenerator();
+}
+
+QString HtmlGenerator::format()
+{
+ return "HTML";
+}
+
+/*!
+ If qdoc is in the \c {-prepare} phase, traverse the primary
+ tree to generate the index file for the current module.
+
+ If qdoc is in the \c {-generate} phase, traverse the primary
+ tree to generate all the HTML documentation for the current
+ module. Then generate the help file and the tag file.
+ */
+void HtmlGenerator::generateDocs()
+{
+ Node *qflags = m_qdb->findClassNode(QStringList("QFlags"));
+ if (qflags)
+ m_qflagsHref = linkForNode(qflags, nullptr);
+ if (!config->preparing())
+ Generator::generateDocs();
+
+ if (!config->generating()) {
+ QString fileBase =
+ m_project.toLower().simplified().replace(QLatin1Char(' '), QLatin1Char('-'));
+ m_qdb->generateIndex(outputDir() + QLatin1Char('/') + fileBase + ".index", m_projectUrl,
+ m_projectDescription, this);
+ }
+
+ if (!config->preparing()) {
+ m_helpProjectWriter->generate();
+ m_manifestWriter->generateManifestFiles();
+ /*
+ Generate the XML tag file, if it was requested.
+ */
+ if (!tagFile_.isEmpty()) {
+ TagFileWriter tagFileWriter;
+ tagFileWriter.generateTagFile(tagFile_, this);
+ }
+ }
+}
+
+/*!
+ Generate an html file with the contents of a C++ or QML source file.
+ */
+void HtmlGenerator::generateExampleFilePage(const Node *en, ResolvedFile resolved_file, CodeMarker *marker)
+{
+ SubTitleSize subTitleSize = LargeSubTitle;
+ QString fullTitle = en->fullTitle();
+
+ beginSubPage(en, linkForExampleFile(resolved_file.get_query()));
+ generateHeader(fullTitle, en, marker);
+ generateTitle(fullTitle, Text() << en->subtitle(), subTitleSize, en, marker);
+
+ Text text;
+ Quoter quoter;
+ Doc::quoteFromFile(en->doc().location(), quoter, resolved_file);
+ QString code = quoter.quoteTo(en->location(), QString(), QString());
+ CodeMarker *codeMarker = CodeMarker::markerForFileName(resolved_file.get_path());
+ text << Atom(codeMarker->atomType(), code);
+ Atom a(codeMarker->atomType(), code);
+
+ generateText(text, en, codeMarker);
+ endSubPage();
+}
+
+/*!
+ Generate html from an instance of Atom.
+ */
+qsizetype HtmlGenerator::generateAtom(const Atom *atom, const Node *relative, CodeMarker *marker)
+{
+ qsizetype idx, skipAhead = 0;
+ static bool in_para = false;
+ Node::Genus genus = Node::DontCare;
+
+ switch (atom->type()) {
+ case Atom::AutoLink: {
+ QString name = atom->string();
+ if (relative && relative->name() == name.replace(QLatin1String("()"), QLatin1String())) {
+ out() << protectEnc(atom->string());
+ break;
+ }
+ // Allow auto-linking to nodes in API reference
+ genus = Node::API;
+ }
+ Q_FALLTHROUGH();
+ case Atom::NavAutoLink:
+ if (!m_inLink && !m_inContents && !m_inSectionHeading) {
+ const Node *node = nullptr;
+ QString link = getAutoLink(atom, relative, &node, genus);
+ if (link.isEmpty()) {
+ if (autolinkErrors() && relative)
+ relative->doc().location().warning(
+ QStringLiteral("Can't autolink to '%1'").arg(atom->string()));
+ } else if (node && node->isDeprecated()) {
+ if (relative && (relative->parent() != node) && !relative->isDeprecated())
+ link.clear();
+ }
+ if (link.isEmpty()) {
+ out() << protectEnc(atom->string());
+ } else {
+ beginLink(link, node, relative);
+ generateLink(atom);
+ endLink();
+ }
+ } else {
+ out() << protectEnc(atom->string());
+ }
+ break;
+ case Atom::BaseName:
+ break;
+ case Atom::BriefLeft:
+ if (!hasBrief(relative)) {
+ skipAhead = skipAtoms(atom, Atom::BriefRight);
+ break;
+ }
+ out() << "<p>";
+ rewritePropertyBrief(atom, relative);
+ break;
+ case Atom::BriefRight:
+ if (hasBrief(relative))
+ out() << "</p>\n";
+ break;
+ case Atom::C:
+ // This may at one time have been used to mark up C++ code but it is
+ // now widely used to write teletype text. As a result, text marked
+ // with the \c command is not passed to a code marker.
+ out() << formattingLeftMap()[ATOM_FORMATTING_TELETYPE];
+ out() << protectEnc(plainCode(atom->string()));
+ out() << formattingRightMap()[ATOM_FORMATTING_TELETYPE];
+ break;
+ case Atom::CaptionLeft:
+ out() << "<p class=\"figCaption\">";
+ in_para = true;
+ break;
+ case Atom::CaptionRight:
+ endLink();
+ if (in_para) {
+ out() << "</p>\n";
+ in_para = false;
+ }
+ break;
+ case Atom::Qml:
+ out() << "<pre class=\"qml\" translate=\"no\">"
+ << trimmedTrailing(highlightedCode(indent(m_codeIndent, atom->string()), relative,
+ false, Node::QML),
+ m_codePrefix, m_codeSuffix)
+ << "</pre>\n";
+ break;
+ case Atom::Code:
+ out() << "<pre class=\"cpp\" translate=\"no\">"
+ << trimmedTrailing(highlightedCode(indent(m_codeIndent, atom->string()), relative),
+ m_codePrefix, m_codeSuffix)
+ << "</pre>\n";
+ break;
+ case Atom::CodeBad:
+ out() << "<pre class=\"cpp plain\" translate=\"no\">"
+ << trimmedTrailing(protectEnc(plainCode(indent(m_codeIndent, atom->string()))),
+ m_codePrefix, m_codeSuffix)
+ << "</pre>\n";
+ break;
+ case Atom::DetailsLeft:
+ out() << "<details>\n";
+ if (!atom->string().isEmpty())
+ out() << "<summary>" << protectEnc(atom->string()) << "</summary>\n";
+ else
+ out() << "<summary>...</summary>\n";
+ break;
+ case Atom::DetailsRight:
+ out() << "</details>\n";
+ break;
+ case Atom::DivLeft:
+ out() << "<div";
+ if (!atom->string().isEmpty())
+ out() << ' ' << atom->string();
+ out() << '>';
+ break;
+ case Atom::DivRight:
+ out() << "</div>";
+ break;
+ case Atom::FootnoteLeft:
+ // ### For now
+ if (in_para) {
+ out() << "</p>\n";
+ in_para = false;
+ }
+ out() << "<!-- ";
+ break;
+ case Atom::FootnoteRight:
+ // ### For now
+ out() << "-->\n";
+ break;
+ case Atom::FormatElse:
+ case Atom::FormatEndif:
+ case Atom::FormatIf:
+ break;
+ case Atom::FormattingLeft:
+ if (atom->string().startsWith("span "))
+ out() << '<' + atom->string() << '>';
+ else
+ out() << formattingLeftMap()[atom->string()];
+ break;
+ case Atom::FormattingRight:
+ if (atom->string() == ATOM_FORMATTING_LINK) {
+ endLink();
+ } else if (atom->string() == ATOM_FORMATTING_TRADEMARK) {
+ if (appendTrademark(atom)) {
+ // Make the trademark symbol a link to navigation.trademarkspage (if set)
+ const Node *node{nullptr};
+ const Atom tm_link(Atom::NavLink, m_trademarkspage);
+ if (const auto &link = getLink(&tm_link, relative, &node);
+ !link.isEmpty() && node != relative)
+ out() << "<a href=\"%1\">%2</a>"_L1.arg(link, formattingRightMap()[atom->string()]);
+ else
+ out() << formattingRightMap()[atom->string()];
+ }
+ } else if (atom->string().startsWith("span ")) {
+ out() << "</span>";
+ } else {
+ out() << formattingRightMap()[atom->string()];
+ }
+ break;
+ case Atom::AnnotatedList: {
+ const CollectionNode *cn = m_qdb->getCollectionNode(atom->string(), Node::Group);
+ if (cn)
+ generateList(cn, marker, atom->string());
+ } break;
+ case Atom::GeneratedList:
+ if (atom->string() == QLatin1String("annotatedclasses")) {
+ generateAnnotatedList(relative, marker, m_qdb->getCppClasses().values());
+ } else if (atom->string() == QLatin1String("annotatedexamples")) {
+ generateAnnotatedLists(relative, marker, m_qdb->getExamples());
+ } else if (atom->string() == QLatin1String("annotatedattributions")) {
+ generateAnnotatedLists(relative, marker, m_qdb->getAttributions());
+ } else if (atom->string() == QLatin1String("classes")) {
+ generateCompactList(Generic, relative, m_qdb->getCppClasses(), true,
+ QStringLiteral(""));
+ } else if (atom->string().contains("classes ")) {
+ QString rootName = atom->string().mid(atom->string().indexOf("classes") + 7).trimmed();
+ generateCompactList(Generic, relative, m_qdb->getCppClasses(), true, rootName);
+ } else if (atom->string() == QLatin1String("qmlvaluetypes")
+ || atom->string() == QLatin1String("qmlbasictypes")) {
+ generateCompactList(Generic, relative, m_qdb->getQmlValueTypes(), true,
+ QStringLiteral(""));
+ } else if (atom->string() == QLatin1String("qmltypes")) {
+ generateCompactList(Generic, relative, m_qdb->getQmlTypes(), true, QStringLiteral(""));
+ } else if ((idx = atom->string().indexOf(QStringLiteral("bymodule"))) != -1) {
+ QDocDatabase *qdb = QDocDatabase::qdocDB();
+ QString moduleName = atom->string().mid(idx + 8).trimmed();
+ Node::NodeType moduleType = typeFromString(atom);
+ if (const auto *cn = qdb->getCollectionNode(moduleName, moduleType)) {
+ NodeMap map;
+ switch (moduleType) {
+ case Node::Module:
+ // classesbymodule <module_name>
+ map = cn->getMembers([](const Node *n) { return n->isClassNode(); });
+ generateAnnotatedList(relative, marker, map.values());
+ break;
+ case Node::QmlModule:
+ if (atom->string().contains(QLatin1String("qmlvaluetypes")))
+ map = cn->getMembers(Node::QmlValueType); // qmlvaluetypesbymodule <module_name>
+ else
+ map = cn->getMembers(Node::QmlType); // qmltypesbymodule <module_name>
+ generateAnnotatedList(relative, marker, map.values());
+ break;
+ default: // fall back to listing all members
+ generateAnnotatedList(relative, marker, cn->members());
+ break;
+ }
+ }
+ } else if (atom->string() == QLatin1String("classhierarchy")) {
+ generateClassHierarchy(relative, m_qdb->getCppClasses());
+ } else if (atom->string() == QLatin1String("obsoleteclasses")) {
+ generateCompactList(Generic, relative, m_qdb->getObsoleteClasses(), false,
+ QStringLiteral("Q"));
+ } else if (atom->string() == QLatin1String("obsoleteqmltypes")) {
+ generateCompactList(Generic, relative, m_qdb->getObsoleteQmlTypes(), false,
+ QStringLiteral(""));
+ } else if (atom->string() == QLatin1String("obsoletecppmembers")) {
+ generateCompactList(Obsolete, relative, m_qdb->getClassesWithObsoleteMembers(), false,
+ QStringLiteral("Q"));
+ } else if (atom->string() == QLatin1String("obsoleteqmlmembers")) {
+ generateCompactList(Obsolete, relative, m_qdb->getQmlTypesWithObsoleteMembers(), false,
+ QStringLiteral(""));
+ } else if (atom->string() == QLatin1String("functionindex")) {
+ generateFunctionIndex(relative);
+ } else if (atom->string() == QLatin1String("attributions")) {
+ generateAnnotatedList(relative, marker, m_qdb->getAttributions().values());
+ } else if (atom->string() == QLatin1String("legalese")) {
+ generateLegaleseList(relative, marker);
+ } else if (atom->string() == QLatin1String("overviews")) {
+ generateList(relative, marker, "overviews");
+ } else if (atom->string() == QLatin1String("cpp-modules")) {
+ generateList(relative, marker, "cpp-modules");
+ } else if (atom->string() == QLatin1String("qml-modules")) {
+ generateList(relative, marker, "qml-modules");
+ } else if (atom->string() == QLatin1String("namespaces")) {
+ generateAnnotatedList(relative, marker, m_qdb->getNamespaces().values());
+ } else if (atom->string() == QLatin1String("related")) {
+ generateList(relative, marker, "related");
+ } else {
+ const CollectionNode *cn = m_qdb->getCollectionNode(atom->string(), Node::Group);
+ if (cn) {
+ if (!generateGroupList(const_cast<CollectionNode *>(cn)))
+ relative->location().warning(
+ QString("'\\generatelist %1' group is empty").arg(atom->string()));
+ } else {
+ relative->location().warning(
+ QString("'\\generatelist %1' no such group").arg(atom->string()));
+ }
+ }
+ break;
+ case Atom::SinceList: {
+ const NodeMultiMap &nsmap = m_qdb->getSinceMap(atom->string());
+ if (nsmap.isEmpty())
+ break;
+
+ const NodeMultiMap &ncmap = m_qdb->getClassMap(atom->string());
+ const NodeMultiMap &nqcmap = m_qdb->getQmlTypeMap(atom->string());
+
+ Sections sections(nsmap);
+ out() << "<ul>\n";
+ const QList<Section> sinceSections = sections.sinceSections();
+ for (const auto &section : sinceSections) {
+ if (!section.members().isEmpty()) {
+ out() << "<li>"
+ << "<a href=\"#" << Utilities::asAsciiPrintable(section.title()) << "\">"
+ << section.title() << "</a></li>\n";
+ }
+ }
+ out() << "</ul>\n";
+
+ int index = 0;
+ for (const auto &section : sinceSections) {
+ if (!section.members().isEmpty()) {
+ out() << "<h3 id=\"" << Utilities::asAsciiPrintable(section.title()) << "\">"
+ << protectEnc(section.title()) << "</h3>\n";
+ if (index == Sections::SinceClasses)
+ generateCompactList(Generic, relative, ncmap, false, QStringLiteral("Q"));
+ else if (index == Sections::SinceQmlTypes)
+ generateCompactList(Generic, relative, nqcmap, false, QStringLiteral(""));
+ else if (index == Sections::SinceMemberFunctions
+ || index == Sections::SinceQmlMethods
+ || index == Sections::SinceQmlProperties) {
+
+ QMap<QString, NodeMultiMap> parentmaps;
+
+ const QList<Node *> &members = section.members();
+ for (const auto &member : members) {
+ QString parent_full_name = (*member).parent()->fullName();
+
+ auto parent_entry = parentmaps.find(parent_full_name);
+ if (parent_entry == parentmaps.end())
+ parent_entry = parentmaps.insert(parent_full_name, NodeMultiMap());
+ parent_entry->insert(member->name(), member);
+ }
+
+ for (auto map = parentmaps.begin(); map != parentmaps.end(); ++map) {
+ NodeVector nv = map->values().toVector();
+ auto parent = nv.front()->parent();
+
+ out() << ((index == Sections::SinceMemberFunctions) ? "<p>Class " : "<p>QML Type ");
+
+ out() << "<a href=\"" << linkForNode(parent, relative) << "\" translate=\"no\">";
+ QStringList pieces = parent->fullName().split("::");
+ out() << protectEnc(pieces.last());
+ out() << "</a>"
+ << ":</p>\n";
+
+ generateSection(nv, relative, marker);
+ out() << "<br/>";
+ }
+ } else if (index == Sections::SinceEnumValues) {
+ out() << "<div class=\"table\"><table class=\"alignedsummary\" translate=\"no\">\n";
+ const auto map_it = m_qdb->newEnumValueMaps().constFind(atom->string());
+ for (auto it = map_it->cbegin(); it != map_it->cend(); ++it) {
+ out() << "<tr><td class=\"memItemLeft\"> enum value </td><td class=\"memItemRight\">"
+ << "<b><a href=\"" << linkForNode(it.value(), nullptr) << "\">"
+ << it.key() << "</a></b></td></tr>\n";
+ }
+ out() << "</table></div>\n";
+ } else {
+ generateSection(section.members(), relative, marker);
+ }
+ }
+ ++index;
+ }
+ } break;
+ case Atom::BR:
+ out() << "<br />\n";
+ break;
+ case Atom::HR:
+ out() << "<hr />\n";
+ break;
+ case Atom::Image:
+ case Atom::InlineImage: {
+ QString text;
+ if (atom->next() && atom->next()->type() == Atom::ImageText)
+ text = atom->next()->string();
+ if (atom->type() == Atom::Image)
+ out() << "<p class=\"centerAlign\">";
+
+ auto maybe_resolved_file{file_resolver.resolve(atom->string())};
+ if (!maybe_resolved_file) {
+ // TODO: [uncentralized-admonition]
+ relative->location().warning(
+ QStringLiteral("Missing image: %1").arg(protectEnc(atom->string())));
+ out() << "<font color=\"red\">[Missing image " << protectEnc(atom->string())
+ << "]</font>";
+ } else {
+ ResolvedFile file{*maybe_resolved_file};
+ QString file_name{QFileInfo{file.get_path()}.fileName()};
+
+ // TODO: [operation-can-fail-making-the-output-incorrect]
+ // The operation of copying the file can fail, making the
+ // output refer to an image that does not exist.
+ // This should be fine as HTML will take care of managing
+ // the rendering of a missing image, but what html will
+ // render is in stark contrast with what we do when the
+ // image does not exist at all.
+ // It may be more correct to unify the behavior between
+ // the two either by considering images that cannot be
+ // copied as missing or letting the HTML renderer
+ // always taking care of the two cases.
+ // Do notice that effectively doing this might be
+ // unnecessary as extracting the output directory logic
+ // should ensure that a safe assumption for copy should be
+ // made at the API boundary.
+
+ // TODO: [uncentralized-output-directory-structure]
+ Config::copyFile(relative->doc().location(), file.get_path(), file_name, outputDir() + QLatin1String("/images"));
+
+ // TODO: [uncentralized-output-directory-structure]
+ out() << "<img src=\"" << "images/" + protectEnc(file_name) << '"';
+
+ // TODO: [same-result-branching]
+ // If text is empty protectEnc should return the empty
+ // string itself, such that the two branches would still
+ // result in the same output.
+ // Ensure that this is the case and then flatten the branch if so.
+ if (!text.isEmpty())
+ out() << " alt=\"" << protectEnc(text) << '"';
+ else
+ out() << " alt=\"\"";
+
+ out() << " />";
+
+ // TODO: [uncentralized-output-directory-structure]
+ m_helpProjectWriter->addExtraFile("images/" + file_name);
+ setImageFileName(relative, "images/" + file_name);
+ }
+
+ if (atom->type() == Atom::Image)
+ out() << "</p>";
+ } break;
+ case Atom::ImageText:
+ break;
+ // Admonitions
+ case Atom::ImportantLeft:
+ case Atom::NoteLeft:
+ case Atom::WarningLeft: {
+ QString admonType = atom->typeString();
+ // Remove 'Left' from atom type to get the admonition type
+ admonType.chop(4);
+ out() << "<div class=\"admonition " << admonType.toLower() << "\">\n"
+ << "<p>";
+ out() << formattingLeftMap()[ATOM_FORMATTING_BOLD];
+ out() << admonType << ": ";
+ out() << formattingRightMap()[ATOM_FORMATTING_BOLD];
+ } break;
+ case Atom::ImportantRight:
+ case Atom::NoteRight:
+ case Atom::WarningRight:
+ out() << "</p>\n"
+ << "</div>\n";
+ break;
+ case Atom::LegaleseLeft:
+ out() << "<div class=\"LegaleseLeft\">";
+ break;
+ case Atom::LegaleseRight:
+ out() << "</div>";
+ break;
+ case Atom::LineBreak:
+ out() << "<br/>";
+ break;
+ case Atom::Link:
+ // Prevent nested links in table of contents
+ if (m_inContents)
+ break;
+ Q_FALLTHROUGH();
+ case Atom::NavLink: {
+ const Node *node = nullptr;
+ QString link = getLink(atom, relative, &node);
+ if (link.isEmpty() && (node != relative) && !noLinkErrors()) {
+ Location location = atom->isLinkAtom() ? static_cast<const LinkAtom*>(atom)->location
+ : relative->doc().location();
+ location.warning(
+ QStringLiteral("Can't link to '%1'").arg(atom->string()));
+ }
+ beginLink(link, node, relative);
+ skipAhead = 1;
+ } break;
+ case Atom::ExampleFileLink: {
+ QString link = linkForExampleFile(atom->string());
+ beginLink(link);
+ skipAhead = 1;
+ } break;
+ case Atom::ExampleImageLink: {
+ QString link = atom->string();
+ link = "images/used-in-examples/" + link;
+ beginLink(link);
+ skipAhead = 1;
+ } break;
+ case Atom::LinkNode: {
+ const Node *node = CodeMarker::nodeForString(atom->string());
+ beginLink(linkForNode(node, relative), node, relative);
+ skipAhead = 1;
+ } break;
+ case Atom::ListLeft:
+ if (in_para) {
+ out() << "</p>\n";
+ in_para = false;
+ }
+ if (atom->string() == ATOM_LIST_BULLET) {
+ out() << "<ul>\n";
+ } else if (atom->string() == ATOM_LIST_TAG) {
+ out() << "<dl>\n";
+ } else if (atom->string() == ATOM_LIST_VALUE) {
+ out() << R"(<div class="table"><table class="valuelist">)";
+ m_threeColumnEnumValueTable = isThreeColumnEnumValueTable(atom);
+ if (m_threeColumnEnumValueTable) {
+ if (++m_numTableRows % 2 == 1)
+ out() << R"(<tr valign="top" class="odd">)";
+ else
+ out() << R"(<tr valign="top" class="even">)";
+
+ out() << "<th class=\"tblConst\">Constant</th>";
+
+ // If not in \enum topic, skip the value column
+ if (relative->isEnumType())
+ out() << "<th class=\"tblval\">Value</th>";
+
+ out() << "<th class=\"tbldscr\">Description</th></tr>\n";
+ } else {
+ out() << "<tr><th class=\"tblConst\">Constant</th><th "
+ "class=\"tblVal\">Value</th></tr>\n";
+ }
+ } else {
+ QString olType;
+ if (atom->string() == ATOM_LIST_UPPERALPHA) {
+ olType = "A";
+ } else if (atom->string() == ATOM_LIST_LOWERALPHA) {
+ olType = "a";
+ } else if (atom->string() == ATOM_LIST_UPPERROMAN) {
+ olType = "I";
+ } else if (atom->string() == ATOM_LIST_LOWERROMAN) {
+ olType = "i";
+ } else { // (atom->string() == ATOM_LIST_NUMERIC)
+ olType = "1";
+ }
+
+ if (atom->next() != nullptr && atom->next()->string().toInt() > 1) {
+ out() << QString(R"(<ol class="%1" type="%1" start="%2">)")
+ .arg(olType, atom->next()->string());
+ } else
+ out() << QString(R"(<ol class="%1" type="%1">)").arg(olType);
+ }
+ break;
+ case Atom::ListItemNumber:
+ break;
+ case Atom::ListTagLeft:
+ if (atom->string() == ATOM_LIST_TAG) {
+ out() << "<dt>";
+ } else { // (atom->string() == ATOM_LIST_VALUE)
+ std::pair<QString, int> pair = getAtomListValue(atom);
+ skipAhead = pair.second;
+ QString t = protectEnc(plainCode(marker->markedUpEnumValue(pair.first, relative)));
+ out() << "<tr><td class=\"topAlign\"><code translate=\"no\">" << t << "</code>";
+
+ if (relative->isEnumType()) {
+ out() << "</td><td class=\"topAlign tblval\">";
+ const auto *enume = static_cast<const EnumNode *>(relative);
+ QString itemValue = enume->itemValue(atom->next()->string());
+ if (itemValue.isEmpty())
+ out() << '?';
+ else
+ out() << "<code translate=\"no\">" << protectEnc(itemValue) << "</code>";
+ }
+ }
+ break;
+ case Atom::SinceTagRight:
+ case Atom::ListTagRight:
+ if (atom->string() == ATOM_LIST_TAG)
+ out() << "</dt>\n";
+ break;
+ case Atom::ListItemLeft:
+ if (atom->string() == ATOM_LIST_TAG) {
+ out() << "<dd>";
+ } else if (atom->string() == ATOM_LIST_VALUE) {
+ if (m_threeColumnEnumValueTable) {
+ out() << "</td><td class=\"topAlign\">";
+ if (matchAhead(atom, Atom::ListItemRight))
+ out() << "&nbsp;";
+ }
+ } else {
+ out() << "<li>";
+ }
+ if (matchAhead(atom, Atom::ParaLeft))
+ skipAhead = 1;
+ break;
+ case Atom::ListItemRight:
+ if (atom->string() == ATOM_LIST_TAG) {
+ out() << "</dd>\n";
+ } else if (atom->string() == ATOM_LIST_VALUE) {
+ out() << "</td></tr>\n";
+ } else {
+ out() << "</li>\n";
+ }
+ break;
+ case Atom::ListRight:
+ if (atom->string() == ATOM_LIST_BULLET) {
+ out() << "</ul>\n";
+ } else if (atom->string() == ATOM_LIST_TAG) {
+ out() << "</dl>\n";
+ } else if (atom->string() == ATOM_LIST_VALUE) {
+ out() << "</table></div>\n";
+ } else {
+ out() << "</ol>\n";
+ }
+ break;
+ case Atom::Nop:
+ break;
+ case Atom::ParaLeft:
+ out() << "<p>";
+ in_para = true;
+ break;
+ case Atom::ParaRight:
+ endLink();
+ if (in_para) {
+ out() << "</p>\n";
+ in_para = false;
+ }
+ // if (!matchAhead(atom, Atom::ListItemRight) && !matchAhead(atom, Atom::TableItemRight))
+ // out() << "</p>\n";
+ break;
+ case Atom::QuotationLeft:
+ out() << "<blockquote>";
+ break;
+ case Atom::QuotationRight:
+ out() << "</blockquote>\n";
+ break;
+ case Atom::RawString:
+ out() << atom->string();
+ break;
+ case Atom::SectionLeft:
+ case Atom::SectionRight:
+ break;
+ case Atom::SectionHeadingLeft: {
+ int unit = atom->string().toInt() + hOffset(relative);
+ out() << "<h" + QString::number(unit) + QLatin1Char(' ') << "id=\""
+ << Tree::refForAtom(atom) << "\">";
+ m_inSectionHeading = true;
+ break;
+ }
+ case Atom::SectionHeadingRight:
+ out() << "</h" + QString::number(atom->string().toInt() + hOffset(relative)) + ">\n";
+ m_inSectionHeading = false;
+ break;
+ case Atom::SidebarLeft:
+ Q_FALLTHROUGH();
+ case Atom::SidebarRight:
+ break;
+ case Atom::String:
+ if (m_inLink && !m_inContents && !m_inSectionHeading) {
+ generateLink(atom);
+ } else {
+ out() << protectEnc(atom->string());
+ }
+ break;
+ case Atom::TableLeft: {
+ std::pair<QString, QString> pair = getTableWidthAttr(atom);
+ QString attr = pair.second;
+ QString width = pair.first;
+
+ if (in_para) {
+ out() << "</p>\n";
+ in_para = false;
+ }
+
+ out() << R"(<div class="table"><table class=")" << attr << '"';
+ if (!width.isEmpty())
+ out() << " width=\"" << width << '"';
+ out() << ">\n ";
+ m_numTableRows = 0;
+ } break;
+ case Atom::TableRight:
+ out() << "</table></div>\n";
+ break;
+ case Atom::TableHeaderLeft:
+ out() << "<thead><tr class=\"qt-style\">";
+ m_inTableHeader = true;
+ break;
+ case Atom::TableHeaderRight:
+ out() << "</tr>";
+ if (matchAhead(atom, Atom::TableHeaderLeft)) {
+ skipAhead = 1;
+ out() << "\n<tr class=\"qt-style\">";
+ } else {
+ out() << "</thead>\n";
+ m_inTableHeader = false;
+ }
+ break;
+ case Atom::TableRowLeft:
+ if (!atom->string().isEmpty())
+ out() << "<tr " << atom->string() << '>';
+ else if (++m_numTableRows % 2 == 1)
+ out() << R"(<tr valign="top" class="odd">)";
+ else
+ out() << R"(<tr valign="top" class="even">)";
+ break;
+ case Atom::TableRowRight:
+ out() << "</tr>\n";
+ break;
+ case Atom::TableItemLeft: {
+ if (m_inTableHeader)
+ out() << "<th ";
+ else
+ out() << "<td ";
+
+ for (int i = 0; i < atom->count(); ++i) {
+ if (i > 0)
+ out() << ' ';
+ const QString &p = atom->string(i);
+ if (p.contains('=')) {
+ out() << p;
+ } else {
+ QStringList spans = p.split(QLatin1Char(','));
+ if (spans.size() == 2) {
+ if (spans.at(0) != "1")
+ out() << " colspan=\"" << spans.at(0) << '"';
+ if (spans.at(1) != "1")
+ out() << " rowspan=\"" << spans.at(1) << '"';
+ }
+ }
+ }
+ out() << '>';
+ if (matchAhead(atom, Atom::ParaLeft))
+ skipAhead = 1;
+ } break;
+ case Atom::TableItemRight:
+ if (m_inTableHeader)
+ out() << "</th>";
+ else {
+ out() << "</td>";
+ }
+ if (matchAhead(atom, Atom::ParaLeft))
+ skipAhead = 1;
+ break;
+ case Atom::TableOfContents:
+ Q_FALLTHROUGH();
+ case Atom::Keyword:
+ break;
+ case Atom::Target:
+ out() << "<span id=\"" << Utilities::asAsciiPrintable(atom->string()) << "\"></span>";
+ break;
+ case Atom::UnhandledFormat:
+ out() << "<b class=\"redFont\">&lt;Missing HTML&gt;</b>";
+ break;
+ case Atom::UnknownCommand:
+ out() << R"(<b class="redFont"><code translate=\"no\">\)" << protectEnc(atom->string()) << "</code></b>";
+ break;
+ case Atom::CodeQuoteArgument:
+ case Atom::CodeQuoteCommand:
+ case Atom::ComparesLeft:
+ case Atom::ComparesRight:
+ case Atom::SnippetCommand:
+ case Atom::SnippetIdentifier:
+ case Atom::SnippetLocation:
+ // no HTML output (ignore)
+ break;
+ default:
+ unknownAtom(atom);
+ }
+ return skipAhead;
+}
+
+/*!
+ * Return a string representing a text that exposes information about
+ * the user-visible groups that the \a node is part of. A user-visible
+ * group is a group that generates an output page, that is, a \\group
+ * topic exists for the group and can be linked to.
+ *
+ * The returned string is composed of comma separated links to the
+ * groups, with their title as the user-facing text, surrounded by
+ * some introductory text.
+ *
+ * For example, if a node named N is part of the groups with title A
+ * and B, the line rendered form of the line will be "N is part of the
+ * A, B groups", where A and B are clickable links that target the
+ * respective page of each group.
+ *
+ * If a node has a single group, the comma is removed for readability
+ * pusposes and "groups" is expressed as a singular noun.
+ * For example, "N is part of the A group".
+ *
+ * The returned string is empty when the node is not linked to any
+ * group that has a valid link target.
+ *
+ * This string is used in the summary of c++ classes or qml types to
+ * link them to some of the overview documentation that is generated
+ * through the "\group" command.
+ *
+ * Note that this is currently, incorrectly, a member of
+ * HtmlGenerator as it requires access to some protected/private
+ * members for escaping and linking.
+ */
+QString HtmlGenerator::groupReferenceText(PageNode* node) {
+ auto link_for_group = [this](const CollectionNode *group) -> QString {
+ QString target{linkForNode(group, nullptr)};
+ return (target.isEmpty()) ? protectEnc(group->name()) : "<a href=\"" + target + "\">" + protectEnc(group->fullTitle()) + "</a>";
+ };
+
+ QString text{};
+
+ const QStringList &groups_names{node->groupNames()};
+ if (groups_names.isEmpty())
+ return text;
+
+ std::vector<CollectionNode *> groups_nodes(groups_names.size(), nullptr);
+ std::transform(groups_names.cbegin(), groups_names.cend(), groups_nodes.begin(),
+ [this](const QString &group_name) -> CollectionNode* {
+ CollectionNode *group{m_qdb->groups()[group_name]};
+ m_qdb->mergeCollections(group);
+ return (group && group->wasSeen()) ? group : nullptr;
+ });
+ groups_nodes.erase(std::remove(groups_nodes.begin(), groups_nodes.end(), nullptr), groups_nodes.end());
+
+ if (!groups_nodes.empty()) {
+ text += node->name() + " is part of ";
+
+ for (std::vector<CollectionNode *>::size_type index{0}; index < groups_nodes.size(); ++index) {
+ text += link_for_group(groups_nodes[index]) + Utilities::separator(index, groups_nodes.size());
+ }
+ }
+ return text;
+}
+
+/*!
+ Generate a reference page for the C++ class, namespace, or
+ header file documented in \a node using the code \a marker
+ provided.
+ */
+void HtmlGenerator::generateCppReferencePage(Aggregate *aggregate, CodeMarker *marker)
+{
+ QString title;
+ QString rawTitle;
+ QString fullTitle;
+ NamespaceNode *ns = nullptr;
+ SectionVector *summarySections = nullptr;
+ SectionVector *detailsSections = nullptr;
+
+ Sections sections(aggregate);
+ QString word = aggregate->typeWord(true);
+ auto templateDecl = aggregate->templateDecl();
+ if (aggregate->isNamespace()) {
+ rawTitle = aggregate->plainName();
+ fullTitle = aggregate->plainFullName();
+ title = rawTitle + " Namespace";
+ ns = static_cast<NamespaceNode *>(aggregate);
+ summarySections = &sections.stdSummarySections();
+ detailsSections = &sections.stdDetailsSections();
+ } else if (aggregate->isClassNode()) {
+ rawTitle = aggregate->plainName();
+ fullTitle = aggregate->plainFullName();
+ title = rawTitle + QLatin1Char(' ') + word;
+ summarySections = &sections.stdCppClassSummarySections();
+ detailsSections = &sections.stdCppClassDetailsSections();
+ } else if (aggregate->isHeader()) {
+ title = fullTitle = rawTitle = aggregate->fullTitle();
+ summarySections = &sections.stdSummarySections();
+ detailsSections = &sections.stdDetailsSections();
+ }
+
+ Text subtitleText;
+ if (rawTitle != fullTitle || templateDecl) {
+ if (aggregate->isClassNode()) {
+ if (templateDecl)
+ subtitleText << (*templateDecl).to_qstring() + QLatin1Char(' ');
+ subtitleText << aggregate->typeWord(false) + QLatin1Char(' ');
+ const QStringList ancestors = fullTitle.split(QLatin1String("::"));
+ for (const auto &a : ancestors) {
+ if (a == rawTitle) {
+ subtitleText << a;
+ break;
+ } else {
+ subtitleText << Atom(Atom::AutoLink, a) << "::";
+ }
+ }
+ } else {
+ subtitleText << fullTitle;
+ }
+ }
+
+ generateHeader(title, aggregate, marker);
+ generateTableOfContents(aggregate, marker, summarySections);
+ generateTitle(title, subtitleText, SmallSubTitle, aggregate, marker);
+ if (ns && !ns->hasDoc() && ns->docNode()) {
+ NamespaceNode *NS = ns->docNode();
+ Text brief;
+ brief << "The " << ns->name() << " namespace includes the following elements from module "
+ << ns->tree()->camelCaseModuleName() << ". The full namespace is "
+ << "documented in module " << NS->tree()->camelCaseModuleName()
+ << Atom(Atom::LinkNode, CodeMarker::stringForNode(NS))
+ << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << Atom(Atom::String, " here.")
+ << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
+ out() << "<p>";
+ generateText(brief, ns, marker);
+ out() << "</p>\n";
+ } else
+ generateBrief(aggregate, marker);
+
+ const auto parentIsClass = aggregate->parent()->isClassNode();
+
+ if (!parentIsClass)
+ generateRequisites(aggregate, marker);
+ generateStatus(aggregate, marker);
+ if (parentIsClass)
+ generateSince(aggregate, marker);
+
+ QString membersLink = generateAllMembersFile(Sections::allMembersSection(), marker);
+ if (!membersLink.isEmpty()) {
+ openUnorderedList();
+ out() << "<li><a href=\"" << membersLink << "\">"
+ << "List of all members, including inherited members</a></li>\n";
+ }
+ QString obsoleteLink = generateObsoleteMembersFile(sections, marker);
+ if (!obsoleteLink.isEmpty()) {
+ openUnorderedList();
+ out() << "<li><a href=\"" << obsoleteLink << "\">"
+ << "Deprecated members</a></li>\n";
+ }
+
+ if (QString groups_text{groupReferenceText(aggregate)}; !groups_text.isEmpty()) {
+ openUnorderedList();
+
+ out() << "<li>" << groups_text << "</li>\n";
+ }
+
+ closeUnorderedList();
+ generateComparisonCategory(aggregate, marker);
+ generateComparisonList(aggregate);
+
+ generateThreadSafeness(aggregate, marker);
+
+ bool needOtherSection = false;
+
+ for (const auto &section : std::as_const(*summarySections)) {
+ if (section.members().isEmpty() && section.reimplementedMembers().isEmpty()) {
+ if (!section.inheritedMembers().isEmpty())
+ needOtherSection = true;
+ } else {
+ if (!section.members().isEmpty()) {
+ QString ref = registerRef(section.title().toLower());
+ out() << "<h2 id=\"" << ref << "\">" << protectEnc(section.title()) << "</h2>\n";
+ generateSection(section.members(), aggregate, marker);
+ }
+ if (!section.reimplementedMembers().isEmpty()) {
+ QString name = QString("Reimplemented ") + section.title();
+ QString ref = registerRef(name.toLower());
+ out() << "<h2 id=\"" << ref << "\">" << protectEnc(name) << "</h2>\n";
+ generateSection(section.reimplementedMembers(), aggregate, marker);
+ }
+
+ if (!section.inheritedMembers().isEmpty()) {
+ out() << "<ul>\n";
+ generateSectionInheritedList(section, aggregate);
+ out() << "</ul>\n";
+ }
+ }
+ }
+
+ if (needOtherSection) {
+ out() << "<h3>Additional Inherited Members</h3>\n"
+ "<ul>\n";
+
+ for (const auto &section : std::as_const(*summarySections)) {
+ if (section.members().isEmpty() && !section.inheritedMembers().isEmpty())
+ generateSectionInheritedList(section, aggregate);
+ }
+ out() << "</ul>\n";
+ }
+
+ if (aggregate->doc().isEmpty()) {
+ QString command = "documentation";
+ if (aggregate->isClassNode())
+ command = R"('\class' comment)";
+ if (!ns || ns->isDocumentedHere()) {
+ aggregate->location().warning(
+ QStringLiteral("No %1 for '%2'").arg(command, aggregate->plainSignature()));
+ }
+ } else {
+ generateExtractionMark(aggregate, DetailedDescriptionMark);
+ out() << "<div class=\"descr\">\n"
+ << "<h2 id=\"" << registerRef("details") << "\">"
+ << "Detailed Description"
+ << "</h2>\n";
+ generateBody(aggregate, marker);
+ out() << "</div>\n";
+ generateAlsoList(aggregate, marker);
+ generateExtractionMark(aggregate, EndMark);
+ }
+
+ for (const auto &section : std::as_const(*detailsSections)) {
+ bool headerGenerated = false;
+ if (section.isEmpty())
+ continue;
+
+ const QList<Node *> &members = section.members();
+ for (const auto &member : members) {
+ if (member->access() == Access::Private) // ### check necessary?
+ continue;
+ if (!headerGenerated) {
+ if (!section.divClass().isEmpty())
+ out() << "<div class=\"" << section.divClass() << "\">\n";
+ out() << "<h2>" << protectEnc(section.title()) << "</h2>\n";
+ headerGenerated = true;
+ }
+ if (!member->isClassNode())
+ generateDetailedMember(member, aggregate, marker);
+ else {
+ out() << "<h3> class ";
+ generateFullName(member, aggregate);
+ out() << "</h3>";
+ generateBrief(member, marker, aggregate);
+ }
+
+ QStringList names;
+ names << member->name();
+ if (member->isFunction()) {
+ const auto *func = reinterpret_cast<const FunctionNode *>(member);
+ if (func->isSomeCtor() || func->isDtor() || func->overloadNumber() != 0)
+ names.clear();
+ } else if (member->isProperty()) {
+ const auto *prop = reinterpret_cast<const PropertyNode *>(member);
+ if (!prop->getters().isEmpty() && !names.contains(prop->getters().first()->name()))
+ names << prop->getters().first()->name();
+ if (!prop->setters().isEmpty())
+ names << prop->setters().first()->name();
+ if (!prop->resetters().isEmpty())
+ names << prop->resetters().first()->name();
+ if (!prop->notifiers().isEmpty())
+ names << prop->notifiers().first()->name();
+ } else if (member->isEnumType()) {
+ const auto *enume = reinterpret_cast<const EnumNode *>(member);
+ if (enume->flagsType())
+ names << enume->flagsType()->name();
+ const auto &enumItemNameList = enume->doc().enumItemNames();
+ const auto &omitEnumItemNameList = enume->doc().omitEnumItemNames();
+ const auto items = QSet<QString>(enumItemNameList.cbegin(), enumItemNameList.cend())
+ - QSet<QString>(omitEnumItemNameList.cbegin(), omitEnumItemNameList.cend());
+ for (const QString &enumName : items) {
+ names << plainCode(marker->markedUpEnumValue(enumName, enume));
+ }
+ }
+ }
+ if (headerGenerated && !section.divClass().isEmpty())
+ out() << "</div>\n";
+ }
+ generateFooter(aggregate);
+}
+
+void HtmlGenerator::generateProxyPage(Aggregate *aggregate, CodeMarker *marker)
+{
+ Q_ASSERT(aggregate->isProxyNode());
+
+ QString title;
+ QString rawTitle;
+ QString fullTitle;
+ Text subtitleText;
+ SectionVector *summarySections = nullptr;
+ SectionVector *detailsSections = nullptr;
+
+ Sections sections(aggregate);
+ rawTitle = aggregate->plainName();
+ fullTitle = aggregate->plainFullName();
+ title = rawTitle + " Proxy Page";
+ summarySections = &sections.stdSummarySections();
+ detailsSections = &sections.stdDetailsSections();
+ generateHeader(title, aggregate, marker);
+ generateTitle(title, subtitleText, SmallSubTitle, aggregate, marker);
+ generateBrief(aggregate, marker);
+ for (auto it = summarySections->constBegin(); it != summarySections->constEnd(); ++it) {
+ if (!it->members().isEmpty()) {
+ QString ref = registerRef(it->title().toLower());
+ out() << "<h2 id=\"" << ref << "\">" << protectEnc(it->title()) << "</h2>\n";
+ generateSection(it->members(), aggregate, marker);
+ }
+ }
+
+ if (!aggregate->doc().isEmpty()) {
+ generateExtractionMark(aggregate, DetailedDescriptionMark);
+ out() << "<div class=\"descr\">\n"
+ << "<h2 id=\"" << registerRef("details") << "\">"
+ << "Detailed Description"
+ << "</h2>\n";
+ generateBody(aggregate, marker);
+ out() << "</div>\n";
+ generateAlsoList(aggregate, marker);
+ generateExtractionMark(aggregate, EndMark);
+ }
+
+ for (const auto &section : std::as_const(*detailsSections)) {
+ if (section.isEmpty())
+ continue;
+
+ if (!section.divClass().isEmpty())
+ out() << "<div class=\"" << section.divClass() << "\">\n";
+ out() << "<h2>" << protectEnc(section.title()) << "</h2>\n";
+
+ const QList<Node *> &members = section.members();
+ for (const auto &member : members) {
+ if (!member->isPrivate()) { // ### check necessary?
+ if (!member->isClassNode())
+ generateDetailedMember(member, aggregate, marker);
+ else {
+ out() << "<h3> class ";
+ generateFullName(member, aggregate);
+ out() << "</h3>";
+ generateBrief(member, marker, aggregate);
+ }
+
+ QStringList names;
+ names << member->name();
+ if (member->isFunction()) {
+ const auto *func = reinterpret_cast<const FunctionNode *>(member);
+ if (func->isSomeCtor() || func->isDtor() || func->overloadNumber() != 0)
+ names.clear();
+ } else if (member->isEnumType()) {
+ const auto *enume = reinterpret_cast<const EnumNode *>(member);
+ if (enume->flagsType())
+ names << enume->flagsType()->name();
+ const auto &enumItemNameList = enume->doc().enumItemNames();
+ const auto &omitEnumItemNameList = enume->doc().omitEnumItemNames();
+ const auto items =
+ QSet<QString>(enumItemNameList.cbegin(), enumItemNameList.cend())
+ - QSet<QString>(omitEnumItemNameList.cbegin(),
+ omitEnumItemNameList.cend());
+ for (const QString &enumName : items)
+ names << plainCode(marker->markedUpEnumValue(enumName, enume));
+ }
+ }
+ }
+ if (!section.divClass().isEmpty())
+ out() << "</div>\n";
+ }
+ generateFooter(aggregate);
+}
+
+/*!
+ Generate the HTML page for a QML type. \qcn is the QML type.
+ \marker is the code markeup object.
+ */
+void HtmlGenerator::generateQmlTypePage(QmlTypeNode *qcn, CodeMarker *marker)
+{
+ Generator::setQmlTypeContext(qcn);
+ SubTitleSize subTitleSize = LargeSubTitle;
+ QString htmlTitle = qcn->fullTitle();
+ if (qcn->isQmlBasicType())
+ htmlTitle.append(" QML Value Type");
+ else
+ htmlTitle.append(" QML Type");
+
+
+ generateHeader(htmlTitle, qcn, marker);
+ Sections sections(qcn);
+ generateTableOfContents(qcn, marker, &sections.stdQmlTypeSummarySections());
+ marker = CodeMarker::markerForLanguage(QLatin1String("QML"));
+ generateTitle(htmlTitle, Text() << qcn->subtitle(), subTitleSize, qcn, marker);
+ generateBrief(qcn, marker);
+ generateQmlRequisites(qcn, marker);
+ generateStatus(qcn, marker);
+
+ QString allQmlMembersLink;
+
+ // No 'All Members' file for QML value types
+ if (!qcn->isQmlBasicType())
+ allQmlMembersLink = generateAllQmlMembersFile(sections, marker);
+ QString obsoleteLink = generateObsoleteQmlMembersFile(sections, marker);
+ if (!allQmlMembersLink.isEmpty() || !obsoleteLink.isEmpty()) {
+ openUnorderedList();
+
+ if (!allQmlMembersLink.isEmpty()) {
+ out() << "<li><a href=\"" << allQmlMembersLink << "\">"
+ << "List of all members, including inherited members</a></li>\n";
+ }
+ if (!obsoleteLink.isEmpty()) {
+ out() << "<li><a href=\"" << obsoleteLink << "\">"
+ << "Deprecated members</a></li>\n";
+ }
+ }
+
+ if (QString groups_text{groupReferenceText(qcn)}; !groups_text.isEmpty()) {
+ openUnorderedList();
+
+ out() << "<li>" << groups_text << "</li>\n";
+ }
+
+ closeUnorderedList();
+
+ const QList<Section> &stdQmlTypeSummarySections = sections.stdQmlTypeSummarySections();
+ for (const auto &section : stdQmlTypeSummarySections) {
+ if (!section.isEmpty()) {
+ QString ref = registerRef(section.title().toLower());
+ out() << "<h2 id=\"" << ref << "\">" << protectEnc(section.title()) << "</h2>\n";
+ generateQmlSummary(section.members(), qcn, marker);
+ }
+ }
+
+ generateExtractionMark(qcn, DetailedDescriptionMark);
+ out() << "<h2 id=\"" << registerRef("details") << "\">"
+ << "Detailed Description"
+ << "</h2>\n";
+ generateBody(qcn, marker);
+ generateAlsoList(qcn, marker);
+ generateExtractionMark(qcn, EndMark);
+
+ const QList<Section> &stdQmlTypeDetailsSections = sections.stdQmlTypeDetailsSections();
+ for (const auto &section : stdQmlTypeDetailsSections) {
+ if (!section.isEmpty()) {
+ out() << "<h2>" << protectEnc(section.title()) << "</h2>\n";
+ const QList<Node *> &members = section.members();
+ for (const auto member : members) {
+ generateDetailedQmlMember(member, qcn, marker);
+ out() << "<br/>\n";
+ }
+ }
+ }
+ generateFooter(qcn);
+ Generator::setQmlTypeContext(nullptr);
+}
+
+/*!
+ Generate the HTML page for an entity that doesn't map
+ to any underlying parsable C++ or QML element.
+ */
+void HtmlGenerator::generatePageNode(PageNode *pn, CodeMarker *marker)
+{
+ SubTitleSize subTitleSize = LargeSubTitle;
+ QString fullTitle = pn->fullTitle();
+
+ generateHeader(fullTitle, pn, marker);
+ /*
+ Generate the TOC for the new doc format.
+ Don't generate a TOC for the home page.
+ */
+ if ((pn->name() != QLatin1String("index.html")))
+ generateTableOfContents(pn, marker, nullptr);
+
+ generateTitle(fullTitle, Text() << pn->subtitle(), subTitleSize, pn, marker);
+ if (pn->isExample()) {
+ generateBrief(pn, marker, nullptr, false);
+ }
+
+ generateExtractionMark(pn, DetailedDescriptionMark);
+ out() << R"(<div class="descr" id=")" << registerRef("details")
+ << "\">\n";
+
+ generateBody(pn, marker);
+ out() << "</div>\n";
+ generateAlsoList(pn, marker);
+ generateExtractionMark(pn, EndMark);
+
+ generateFooter(pn);
+}
+
+/*!
+ Generate the HTML page for a group, module, or QML module.
+ */
+void HtmlGenerator::generateCollectionNode(CollectionNode *cn, CodeMarker *marker)
+{
+ SubTitleSize subTitleSize = LargeSubTitle;
+ QString fullTitle = cn->fullTitle();
+ QString ref;
+
+ generateHeader(fullTitle, cn, marker);
+ generateTableOfContents(cn, marker, nullptr);
+ generateTitle(fullTitle, Text() << cn->subtitle(), subTitleSize, cn, marker);
+
+ // Generate brief for C++ modules, status for all modules.
+ if (cn->genus() != Node::DOC && cn->genus() != Node::DontCare) {
+ if (cn->isModule())
+ generateBrief(cn, marker);
+ generateStatus(cn, marker);
+ generateSince(cn, marker);
+ }
+
+ if (cn->isModule()) {
+ if (!cn->noAutoList()) {
+ NodeMap nmm{cn->getMembers(Node::Namespace)};
+ if (!nmm.isEmpty()) {
+ ref = registerRef("namespaces");
+ out() << "<h2 id=\"" << ref << "\">Namespaces</h2>\n";
+ generateAnnotatedList(cn, marker, nmm.values());
+ }
+ nmm = cn->getMembers([](const Node *n){ return n->isClassNode(); });
+ if (!nmm.isEmpty()) {
+ ref = registerRef("classes");
+ out() << "<h2 id=\"" << ref << "\">Classes</h2>\n";
+ generateAnnotatedList(cn, marker, nmm.values());
+ }
+ }
+ }
+
+ if (cn->isModule() && !cn->doc().briefText().isEmpty()) {
+ generateExtractionMark(cn, DetailedDescriptionMark);
+ ref = registerRef("details");
+ out() << "<div class=\"descr\">\n";
+ out() << "<h2 id=\"" << ref << "\">"
+ << "Detailed Description"
+ << "</h2>\n";
+ } else {
+ generateExtractionMark(cn, DetailedDescriptionMark);
+ out() << R"(<div class="descr" id=")" << registerRef("details")
+ << "\">\n";
+ }
+
+ generateBody(cn, marker);
+ out() << "</div>\n";
+ generateAlsoList(cn, marker);
+ generateExtractionMark(cn, EndMark);
+
+ if (!cn->noAutoList()) {
+ if (cn->isGroup() || cn->isQmlModule())
+ generateAnnotatedList(cn, marker, cn->members());
+ }
+ generateFooter(cn);
+}
+
+/*!
+ Generate the HTML page for a generic collection. This is usually
+ a collection of C++ elements that are related to an element in
+ a different module.
+ */
+void HtmlGenerator::generateGenericCollectionPage(CollectionNode *cn, CodeMarker *marker)
+{
+ SubTitleSize subTitleSize = LargeSubTitle;
+ QString fullTitle = cn->name();
+
+ generateHeader(fullTitle, cn, marker);
+ generateTitle(fullTitle, Text() << cn->subtitle(), subTitleSize, cn, marker);
+
+ Text brief;
+ brief << "Each function or type documented here is related to a class or "
+ << "namespace that is documented in a different module. The reference "
+ << "page for that class or namespace will link to the function or type "
+ << "on this page.";
+ out() << "<p>";
+ generateText(brief, cn, marker);
+ out() << "</p>\n";
+
+ const QList<Node *> members = cn->members();
+ for (const auto &member : members)
+ generateDetailedMember(member, cn, marker);
+
+ generateFooter(cn);
+}
+
+/*!
+ Returns "html" for this subclass of Generator.
+ */
+QString HtmlGenerator::fileExtension() const
+{
+ return "html";
+}
+
+/*!
+ Output a navigation bar (breadcrumbs) for the html file.
+ For API reference pages, items for the navigation bar are (in order):
+ \table
+ \header \li Item \li Related configuration variable \li Notes
+ \row \li home \li navigation.homepage \li e.g. 'Qt 6.2'
+ \row \li landing \li navigation.landingpage \li Module landing page
+ \row \li types \li navigation.cppclassespage (C++)\br
+ navigation.qmltypespage (QML) \li Types only
+ \row \li module \li n/a (automatic) \li Module page if different
+ from previous item
+ \row \li page \li n/a \li Current page title
+ \endtable
+
+ For other page types (page nodes) the navigation bar is constructed from home
+ page, landing page, and the chain of PageNode::navigationParent() items (if one exists).
+ This chain is constructed from the \\list structure on a page or pages defined in
+ \c navigation.toctitles configuration variable.
+
+ Finally, if no other navigation data exists for a page but it is a member of a
+ single group (using \\ingroup), add that group page to the navigation bar.
+ */
+void HtmlGenerator::generateNavigationBar(const QString &title, const Node *node,
+ CodeMarker *marker, const QString &buildversion,
+ bool tableItems)
+{
+ if (m_noNavigationBar || node == nullptr)
+ return;
+
+ Text navigationbar;
+
+ // Set list item types based on the navigation bar type
+ // TODO: Do we still need table items?
+ Atom::AtomType itemLeft = tableItems ? Atom::TableItemLeft : Atom::ListItemLeft;
+ Atom::AtomType itemRight = tableItems ? Atom::TableItemRight : Atom::ListItemRight;
+
+ // Helper to add an item to navigation bar based on a string link target
+ auto addNavItem = [&](const QString &link, const QString &title) {
+ navigationbar << Atom(itemLeft) << Atom(Atom::NavLink, link)
+ << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
+ << Atom(Atom::String, title)
+ << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom(itemRight);
+ };
+
+ // Helper to add an item to navigation bar based on a target node
+ auto addNavItemNode = [&](const Node *node, const QString &title) {
+ navigationbar << Atom(itemLeft) << Atom(Atom::LinkNode, CodeMarker::stringForNode(node))
+ << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
+ << Atom(Atom::String, title)
+ << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom(itemRight);
+ };
+
+ // Resolve the associated module (collection) node and its 'state' description
+ const auto *moduleNode = m_qdb->getModuleNode(node);
+ QString moduleState;
+ if (moduleNode && !moduleNode->state().isEmpty())
+ moduleState = QStringLiteral(" (%1)").arg(moduleNode->state());
+
+ if (m_hometitle == title)
+ return;
+ if (!m_homepage.isEmpty())
+ addNavItem(m_homepage, m_hometitle);
+ if (!m_landingpage.isEmpty() && m_landingtitle != title)
+ addNavItem(m_landingpage, m_landingtitle);
+
+ if (node->isClassNode()) {
+ if (!m_cppclassespage.isEmpty() && !m_cppclassestitle.isEmpty())
+ addNavItem(m_cppclassespage, m_cppclassestitle);
+ if (!node->physicalModuleName().isEmpty()) {
+ // Add explicit link to the \module page if:
+ // - It's not the C++ classes page that's already added, OR
+ // - It has a \modulestate associated with it
+ if (moduleNode && (!moduleState.isEmpty() || moduleNode->title() != m_cppclassespage))
+ addNavItemNode(moduleNode, moduleNode->name() + moduleState);
+ }
+ navigationbar << Atom(itemLeft) << Atom(Atom::String, node->name()) << Atom(itemRight);
+ } else if (node->isQmlType()) {
+ if (!m_qmltypespage.isEmpty() && !m_qmltypestitle.isEmpty())
+ addNavItem(m_qmltypespage, m_qmltypestitle);
+ // Add explicit link to the \qmlmodule page if:
+ // - It's not the QML types page that's already added, OR
+ // - It has a \modulestate associated with it
+ if (moduleNode && (!moduleState.isEmpty() || moduleNode->title() != m_qmltypespage)) {
+ addNavItemNode(moduleNode, moduleNode->name() + moduleState);
+ }
+ navigationbar << Atom(itemLeft) << Atom(Atom::String, node->name()) << Atom(itemRight);
+ } else {
+ if (node->isPageNode()) {
+ auto currentNode{static_cast<const PageNode*>(node)};
+ std::deque<const Node *> navNodes;
+ // Cutoff at 16 items in case there's a circular dependency
+ qsizetype navItems = 0;
+ while (currentNode->navigationParent() && ++navItems < 16) {
+ if (std::find(navNodes.cbegin(), navNodes.cend(),
+ currentNode->navigationParent()) == navNodes.cend())
+ navNodes.push_front(currentNode->navigationParent());
+ currentNode = currentNode->navigationParent();
+ }
+ // If no nav. parent was found but the page is a \group member, add a link to the
+ // (first) group page.
+ if (navNodes.empty()) {
+ const QStringList groups = static_cast<const PageNode *>(node)->groupNames();
+ for (const auto &groupName : groups) {
+ const auto *groupNode = m_qdb->findNodeByNameAndType(QStringList{groupName}, &Node::isGroup);
+ if (groupNode && !groupNode->title().isEmpty()) {
+ navNodes.push_front(groupNode);
+ break;
+ }
+ }
+ }
+ while (!navNodes.empty()) {
+ if (navNodes.front()->isPageNode())
+ addNavItemNode(navNodes.front(), navNodes.front()->title());
+ navNodes.pop_front();
+ }
+ }
+ if (!navigationbar.isEmpty()) {
+ navigationbar << Atom(itemLeft) << Atom(Atom::String, title) << Atom(itemRight);
+ }
+ }
+
+ generateText(navigationbar, node, marker);
+
+ if (buildversion.isEmpty())
+ return;
+
+ navigationbar.clear();
+
+ if (tableItems) {
+ out() << "</tr></table><table class=\"buildversion\"><tr>\n"
+ << R"(<td id="buildversion" width="100%" align="right">)";
+ } else {
+ out() << "<li id=\"buildversion\">";
+ }
+
+ // Link buildversion string to navigation.landingpage
+ if (!m_landingpage.isEmpty() && m_landingtitle != title) {
+ navigationbar << Atom(Atom::NavLink, m_landingpage)
+ << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
+ << Atom(Atom::String, buildversion)
+ << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
+ generateText(navigationbar, node, marker);
+ } else {
+ out() << buildversion;
+ }
+ if (tableItems)
+ out() << "</td>\n";
+ else
+ out() << "</li>\n";
+}
+
+void HtmlGenerator::generateHeader(const QString &title, const Node *node, CodeMarker *marker)
+{
+ out() << "<!DOCTYPE html>\n";
+ out() << QString("<html lang=\"%1\">\n").arg(naturalLanguage);
+ out() << "<head>\n";
+ out() << " <meta charset=\"utf-8\">\n";
+ if (node && !node->doc().location().isEmpty())
+ out() << "<!-- " << node->doc().location().fileName() << " -->\n";
+
+ if (node && !node->doc().briefText().isEmpty()) {
+ out() << " <meta name=\"description\" content=\""
+ << protectEnc(node->doc().briefText().toString())
+ << "\">\n";
+ }
+
+ // determine the rest of the <title> element content: "title | titleSuffix version"
+ QString titleSuffix;
+ if (!m_landingtitle.isEmpty()) {
+ // for normal pages: "title | landingtitle version"
+ titleSuffix = m_landingtitle;
+ } else if (!m_hometitle.isEmpty()) {
+ // for pages that set the homepage title but not landing page title:
+ // "title | hometitle version"
+ if (title != m_hometitle)
+ titleSuffix = m_hometitle;
+ } else if (!m_project.isEmpty()) {
+ // for projects outside of Qt or Qt 5: "title | project version"
+ if (title != m_project)
+ titleSuffix = m_project;
+ } else
+ // default: "title | Qt version"
+ titleSuffix = QLatin1String("Qt ");
+
+ if (title == titleSuffix)
+ titleSuffix.clear();
+
+ QString divider;
+ if (!titleSuffix.isEmpty() && !title.isEmpty())
+ divider = QLatin1String(" | ");
+
+ // Generating page title
+ out() << " <title>" << protectEnc(title) << divider << titleSuffix;
+
+ // append a full version to the suffix if neither suffix nor title
+ // include (a prefix of) version information
+ QVersionNumber projectVersion = QVersionNumber::fromString(m_qdb->version());
+ if (!projectVersion.isNull()) {
+ QVersionNumber titleVersion;
+ static const QRegularExpression re(QLatin1String(R"(\d+\.\d+)"));
+ const QString &versionedTitle = titleSuffix.isEmpty() ? title : titleSuffix;
+ auto match = re.match(versionedTitle);
+ if (match.hasMatch())
+ titleVersion = QVersionNumber::fromString(match.captured());
+ if (titleVersion.isNull() || !titleVersion.isPrefixOf(projectVersion))
+ out() << QLatin1Char(' ') << projectVersion.toString();
+ }
+ out() << "</title>\n";
+
+ // Include style sheet and script links.
+ out() << m_headerStyles;
+ out() << m_headerScripts;
+ if (m_endHeader.isEmpty())
+ out() << "</head>\n<body>\n";
+ else
+ out() << m_endHeader;
+
+ out() << QString(m_postHeader).replace("\\" + COMMAND_VERSION, m_qdb->version());
+ bool usingTable = m_postHeader.trimmed().endsWith(QLatin1String("<tr>"));
+ generateNavigationBar(title, node, marker, m_buildversion, usingTable);
+ out() << QString(m_postPostHeader).replace("\\" + COMMAND_VERSION, m_qdb->version());
+
+ m_navigationLinks.clear();
+ refMap.clear();
+
+ if (node && !node->links().empty()) {
+ std::pair<QString, QString> linkPair;
+ std::pair<QString, QString> anchorPair;
+ const Node *linkNode;
+ bool useSeparator = false;
+
+ if (node->links().contains(Node::PreviousLink)) {
+ linkPair = node->links()[Node::PreviousLink];
+ linkNode = m_qdb->findNodeForTarget(linkPair.first, node);
+ if (linkNode == nullptr && !noLinkErrors())
+ node->doc().location().warning(
+ QStringLiteral("Cannot link to '%1'").arg(linkPair.first));
+ if (linkNode == nullptr || linkNode == node)
+ anchorPair = linkPair;
+ else
+ anchorPair = anchorForNode(linkNode);
+
+ out() << R"( <link rel="prev" href=")" << anchorPair.first << "\" />\n";
+
+ m_navigationLinks += R"(<a class="prevPage" href=")" + anchorPair.first + "\">";
+ if (linkPair.first == linkPair.second && !anchorPair.second.isEmpty())
+ m_navigationLinks += protect(anchorPair.second);
+ else
+ m_navigationLinks += protect(linkPair.second);
+ m_navigationLinks += "</a>\n";
+ useSeparator = !m_navigationSeparator.isEmpty();
+ }
+ if (node->links().contains(Node::NextLink)) {
+ linkPair = node->links()[Node::NextLink];
+ linkNode = m_qdb->findNodeForTarget(linkPair.first, node);
+ if (linkNode == nullptr && !noLinkErrors())
+ node->doc().location().warning(
+ QStringLiteral("Cannot link to '%1'").arg(linkPair.first));
+ if (linkNode == nullptr || linkNode == node)
+ anchorPair = linkPair;
+ else
+ anchorPair = anchorForNode(linkNode);
+
+ out() << R"( <link rel="next" href=")" << anchorPair.first << "\" />\n";
+
+ if (useSeparator)
+ m_navigationLinks += m_navigationSeparator;
+
+ m_navigationLinks += R"(<a class="nextPage" href=")" + anchorPair.first + "\">";
+ if (linkPair.first == linkPair.second && !anchorPair.second.isEmpty())
+ m_navigationLinks += protect(anchorPair.second);
+ else
+ m_navigationLinks += protect(linkPair.second);
+ m_navigationLinks += "</a>\n";
+ }
+ if (node->links().contains(Node::StartLink)) {
+ linkPair = node->links()[Node::StartLink];
+ linkNode = m_qdb->findNodeForTarget(linkPair.first, node);
+ if (linkNode == nullptr && !noLinkErrors())
+ node->doc().location().warning(
+ QStringLiteral("Cannot link to '%1'").arg(linkPair.first));
+ if (linkNode == nullptr || linkNode == node)
+ anchorPair = linkPair;
+ else
+ anchorPair = anchorForNode(linkNode);
+ out() << R"( <link rel="start" href=")" << anchorPair.first << "\" />\n";
+ }
+ }
+
+ if (node && !node->links().empty())
+ out() << "<p class=\"naviNextPrevious headerNavi\">\n" << m_navigationLinks << "</p>\n";
+}
+
+void HtmlGenerator::generateTitle(const QString &title, const Text &subtitle,
+ SubTitleSize subTitleSize, const Node *relative,
+ CodeMarker *marker)
+{
+ out() << QString(m_prologue).replace("\\" + COMMAND_VERSION, m_qdb->version());
+ QString attribute;
+ if (relative->genus() & Node::API)
+ attribute = R"( translate="no")";
+
+ if (!title.isEmpty())
+ out() << "<h1 class=\"title\"" << attribute << ">" << protectEnc(title) << "</h1>\n";
+ if (!subtitle.isEmpty()) {
+ out() << "<span";
+ if (subTitleSize == SmallSubTitle)
+ out() << " class=\"small-subtitle\"" << attribute << ">";
+ else
+ out() << " class=\"subtitle\"" << attribute << ">";
+ generateText(subtitle, relative, marker);
+ out() << "</span>\n";
+ }
+}
+
+void HtmlGenerator::generateFooter(const Node *node)
+{
+ if (node && !node->links().empty())
+ out() << "<p class=\"naviNextPrevious footerNavi\">\n" << m_navigationLinks << "</p>\n";
+
+ out() << QString(m_footer).replace("\\" + COMMAND_VERSION, m_qdb->version())
+ << QString(m_address).replace("\\" + COMMAND_VERSION, m_qdb->version());
+
+ out() << "</body>\n";
+ out() << "</html>\n";
+}
+
+/*!
+Lists the required imports and includes in a table.
+The number of rows is known.
+*/
+void HtmlGenerator::generateRequisites(Aggregate *aggregate, CodeMarker *marker)
+{
+ QMap<QString, Text> requisites;
+ Text text;
+
+ const QString headerText = "Header";
+ const QString sinceText = "Since";
+ const QString inheritedBytext = "Inherited By";
+ const QString inheritsText = "Inherits";
+ const QString instantiatedByText = "Instantiated By";
+ const QString qtVariableText = "qmake";
+ const QString cmakeText = "CMake";
+ const QString statusText = "Status";
+
+ // The order of the requisites matter
+ const QStringList requisiteorder { headerText, cmakeText, qtVariableText, sinceText,
+ instantiatedByText, inheritsText, inheritedBytext, statusText };
+
+ addIncludeFileToMap(aggregate, marker, requisites, text, headerText);
+ addSinceToMap(aggregate, requisites, &text, sinceText);
+
+ if (aggregate->isClassNode() || aggregate->isNamespace()) {
+ addCMakeInfoToMap(aggregate, requisites, &text, cmakeText);
+ addQtVariableToMap(aggregate, requisites, &text, qtVariableText);
+ }
+
+ if (aggregate->isClassNode()) {
+ auto *classe = dynamic_cast<ClassNode *>(aggregate);
+ if (classe->qmlElement() != nullptr && !classe->isInternal())
+ addInstantiatedByToMap(requisites, &text, instantiatedByText, classe);
+
+ addInheritsToMap(requisites, &text, inheritsText, classe);
+ addInheritedByToMap(requisites, &text, inheritedBytext, classe);
+ }
+
+ // Add the state description (if any) to the map
+ addStatusToMap(aggregate, requisites, text, statusText);
+
+ if (!requisites.isEmpty()) {
+ // generate the table
+ generateTheTable(requisiteorder, requisites, headerText, aggregate, marker);
+ }
+}
+
+/*!
+ * \internal
+ */
+void HtmlGenerator::generateTheTable(const QStringList &requisiteOrder,
+ const QMap<QString, Text> &requisites,
+ const QString &headerText, const Aggregate *aggregate,
+ CodeMarker *marker)
+{
+ out() << "<div class=\"table\"><table class=\"alignedsummary\" translate=\"no\">\n";
+
+ for (auto it = requisiteOrder.constBegin(); it != requisiteOrder.constEnd(); ++it) {
+
+ if (requisites.contains(*it)) {
+ out() << "<tr>"
+ << "<td class=\"memItemLeft rightAlign topAlign\"> " << *it
+ << ":"
+ "</td><td class=\"memItemRight bottomAlign\"> ";
+
+ if (*it == headerText)
+ out() << requisites.value(*it).toString();
+ else
+ generateText(requisites.value(*it), aggregate, marker);
+ out() << "</td></tr>\n";
+ }
+ }
+ out() << "</table></div>\n";
+}
+
+/*!
+ * \internal
+ * Adds inherited by information to the map.
+ */
+void HtmlGenerator::addInheritedByToMap(QMap<QString, Text> &requisites, Text *text,
+ const QString &inheritedBytext, ClassNode *classe)
+{
+ if (!classe->derivedClasses().isEmpty()) {
+ text->clear();
+ *text << Atom::ParaLeft;
+ int count = appendSortedNames(*text, classe, classe->derivedClasses());
+ *text << Atom::ParaRight;
+ if (count > 0)
+ requisites.insert(inheritedBytext, *text);
+ }
+}
+
+/*!
+ * \internal
+ * Adds base classes to the map.
+ */
+void HtmlGenerator::addInheritsToMap(QMap<QString, Text> &requisites, Text *text,
+ const QString &inheritsText, ClassNode *classe)
+{
+ if (!classe->baseClasses().isEmpty()) {
+ int index = 0;
+ text->clear();
+ const auto baseClasses = classe->baseClasses();
+ for (const auto &cls : baseClasses) {
+ if (cls.m_node) {
+ appendFullName(*text, cls.m_node, classe);
+
+ if (cls.m_access == Access::Protected) {
+ *text << " (protected)";
+ } else if (cls.m_access == Access::Private) {
+ *text << " (private)";
+ }
+ *text << Utilities::comma(index++, classe->baseClasses().size());
+ }
+ }
+ *text << Atom::ParaRight;
+ if (index > 0)
+ requisites.insert(inheritsText, *text);
+ }
+}
+
+/*!
+ * \internal
+ * Add the instantiated by information to the map.
+ */
+void HtmlGenerator::addInstantiatedByToMap(QMap<QString, Text> &requisites, Text *text,
+ const QString &instantiatedByText,
+ ClassNode *classe) const
+{
+ if (text != nullptr) {
+ text->clear();
+ *text << Atom(Atom::LinkNode, CodeMarker::stringForNode(classe->qmlElement()))
+ << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
+ << Atom(Atom::String, classe->qmlElement()->name())
+ << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
+ requisites.insert(instantiatedByText, *text);
+ }
+}
+
+/*!
+ * \internal
+ * Adds the CMake package and link library information to the map.
+ */
+void HtmlGenerator::addCMakeInfoToMap(const Aggregate *aggregate, QMap<QString, Text> &requisites,
+ Text *text, const QString &CMakeInfo) const
+{
+ if (!aggregate->physicalModuleName().isEmpty() && text != nullptr) {
+ const CollectionNode *cn =
+ m_qdb->getCollectionNode(aggregate->physicalModuleName(), Node::Module);
+ if (!cn || cn->qtCMakeComponent().isEmpty())
+ return;
+
+ text->clear();
+ const QString qtComponent = "Qt" + QString::number(QT_VERSION_MAJOR);
+ const QString findPackageText = "find_package(" + qtComponent + " REQUIRED COMPONENTS "
+ + cn->qtCMakeComponent() + ")";
+ const QString targetText = cn->qtCMakeTargetItem().isEmpty() ? cn->qtCMakeComponent() : cn->qtCMakeTargetItem();
+ const QString targetLinkLibrariesText = "target_link_libraries(mytarget PRIVATE "
+ + qtComponent + "::" + targetText + ")";
+ const Atom lineBreak = Atom(Atom::RawString, " <br/>\n");
+ *text << findPackageText << lineBreak << targetLinkLibrariesText;
+ requisites.insert(CMakeInfo, *text);
+ }
+}
+
+/*!
+ * \internal
+ * Adds the Qt variable (from the \\qtvariable command) to the map.
+ */
+void HtmlGenerator::addQtVariableToMap(const Aggregate *aggregate, QMap<QString, Text> &requisites,
+ Text *text, const QString &qtVariableText) const
+{
+ if (!aggregate->physicalModuleName().isEmpty()) {
+ const CollectionNode *cn =
+ m_qdb->getCollectionNode(aggregate->physicalModuleName(), Node::Module);
+
+ if (cn && !cn->qtVariable().isEmpty()) {
+ text->clear();
+ *text << "QT += " + cn->qtVariable();
+ requisites.insert(qtVariableText, *text);
+ }
+ }
+}
+
+/*!
+ * \internal
+ * Adds the since information (from the \\since command) to the map.
+ *
+ */
+void HtmlGenerator::addSinceToMap(const Aggregate *aggregate, QMap<QString, Text> &requisites,
+ Text *text, const QString &sinceText) const
+{
+ if (!aggregate->since().isEmpty() && text != nullptr) {
+ text->clear();
+ *text << formatSince(aggregate) << Atom::ParaRight;
+ requisites.insert(sinceText, *text);
+ }
+}
+
+/*!
+ * \internal
+ * Adds the status description for \a aggregate, together with a <span> element, to the \a
+ * requisites map.
+ *
+ * The span element can be used for adding CSS styling/icon associated with a specific status.
+ * The span class name is constructed by converting the description (sans \\deprecated
+ * version info) to lowercase and replacing all non-alphanum characters with hyphens. In
+ * addition, the span has a class \c status. For example,
+ * 'Tech Preview' -> class="status tech-preview"
+*/
+void HtmlGenerator::addStatusToMap(const Aggregate *aggregate, QMap<QString, Text> &requisites,
+ Text &text, const QString &statusText) const
+{
+ auto status{formatStatus(aggregate, m_qdb)};
+ if (!status)
+ return;
+
+ QString spanClass;
+ if (aggregate->status() == Node::Deprecated)
+ spanClass = u"deprecated"_s; // Disregard any version info
+ else
+ spanClass = Utilities::asAsciiPrintable(status.value());
+
+ text.clear();
+ text << Atom(Atom::String, status.value())
+ << Atom(Atom::FormattingLeft, ATOM_FORMATTING_SPAN +
+ "class=\"status %1\""_L1.arg(spanClass))
+ << Atom(Atom::FormattingRight, ATOM_FORMATTING_SPAN);
+ requisites.insert(statusText, text);
+}
+
+/*!
+ * \internal
+ * Adds the includes (from the \\includefile command) to the map.
+ */
+void HtmlGenerator::addIncludeFileToMap(const Aggregate *aggregate, CodeMarker *marker,
+ QMap<QString, Text> &requisites, Text& text,
+ const QString &headerText)
+{
+ if (aggregate->includeFile()) {
+ text.clear();
+ text << highlightedCode(
+ indent(m_codeIndent, marker->markedUpInclude(*aggregate->includeFile())),
+ aggregate
+ );
+
+ requisites.insert(headerText, text);
+ }
+}
+
+/*!
+Lists the required imports and includes in a table.
+The number of rows is known.
+*/
+void HtmlGenerator::generateQmlRequisites(QmlTypeNode *qcn, CodeMarker *marker)
+{
+ if (qcn == nullptr)
+ return;
+ QMap<QString, Text> requisites;
+ Text text;
+
+ const QString importText = "Import Statement:";
+ const QString sinceText = "Since:";
+ const QString inheritedBytext = "Inherited By:";
+ const QString inheritsText = "Inherits:";
+ const QString instantiatesText = "Instantiates:";
+ const QString statusText = "Status:";
+
+ // add the module name and version to the map
+ QString logicalModuleVersion;
+ const CollectionNode *collection = qcn->logicalModule();
+
+ // skip import statement of \internal collections
+ if (!qcn->logicalModuleName().isEmpty() && (!collection || !collection->isInternal() || m_showInternal)) {
+ QStringList parts = QStringList() << "import" << qcn->logicalModuleName() << qcn->logicalModuleVersion();
+ text.clear();
+ text << parts.join(' ').trimmed();
+ requisites.insert(importText, text);
+ } else if (!qcn->isQmlBasicType() && qcn->logicalModuleName().isEmpty()) {
+ qcn->doc().location().warning(QStringLiteral("Could not resolve QML import statement for type '%1'").arg(qcn->name()),
+ QStringLiteral("Maybe you forgot to use the '\\%1' command?").arg(COMMAND_INQMLMODULE));
+ }
+
+ // add the since and project into the map
+ if (!qcn->since().isEmpty()) {
+ text.clear();
+ text << formatSince(qcn) << Atom::ParaRight;
+ requisites.insert(sinceText, text);
+ }
+
+ // add the instantiates to the map
+ ClassNode *cn = qcn->classNode();
+ if (cn && !cn->isInternal()) {
+ text.clear();
+ text << Atom(Atom::LinkNode, CodeMarker::stringForNode(cn));
+ text << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK);
+ text << Atom(Atom::String, cn->name());
+ text << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
+ requisites.insert(instantiatesText, text);
+ }
+
+ // add the inherits to the map
+ QmlTypeNode *base = qcn->qmlBaseNode();
+ while (base && base->isInternal()) {
+ base = base->qmlBaseNode();
+ }
+ if (base) {
+ text.clear();
+ text << Atom::ParaLeft << Atom(Atom::LinkNode, CodeMarker::stringForNode(base))
+ << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << Atom(Atom::String, base->name())
+ << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom::ParaRight;
+ requisites.insert(inheritsText, text);
+ }
+
+ // add the inherited-by to the map
+ NodeList subs;
+ QmlTypeNode::subclasses(qcn, subs);
+ if (!subs.isEmpty()) {
+ text.clear();
+ text << Atom::ParaLeft;
+ int count = appendSortedQmlNames(text, qcn, subs);
+ text << Atom::ParaRight;
+ if (count > 0)
+ requisites.insert(inheritedBytext, text);
+ }
+
+ // Add the state description (if any) to the map
+ addStatusToMap(qcn, requisites, text, statusText);
+
+ // The order of the requisites matter
+ const QStringList requisiteorder { importText, sinceText, instantiatesText, inheritsText,
+ inheritedBytext, statusText };
+
+ if (!requisites.isEmpty()) {
+ // generate the table
+ out() << "<div class=\"table\"><table class=\"alignedsummary\" translate=\"no\">\n";
+ for (const auto &requisite : requisiteorder) {
+
+ if (requisites.contains(requisite)) {
+ out() << "<tr>"
+ << "<td class=\"memItemLeft rightAlign topAlign\"> " << requisite
+ << "</td><td class=\"memItemRight bottomAlign\"> ";
+
+ if (requisite == importText)
+ out() << requisites.value(requisite).toString();
+ else
+ generateText(requisites.value(requisite), qcn, marker);
+ out() << "</td></tr>";
+ }
+ }
+ out() << "</table></div>";
+ }
+}
+
+void HtmlGenerator::generateBrief(const Node *node, CodeMarker *marker, const Node *relative,
+ bool addLink)
+{
+ Text brief = node->doc().briefText();
+
+ if (!brief.isEmpty()) {
+ if (!brief.lastAtom()->string().endsWith('.')) {
+ brief << Atom(Atom::String, ".");
+ node->doc().location().warning(
+ QStringLiteral("'\\brief' statement does not end with a full stop."));
+ }
+ generateExtractionMark(node, BriefMark);
+ out() << "<p>";
+ generateText(brief, node, marker);
+
+ if (addLink) {
+ if (!relative || node == relative)
+ out() << " <a href=\"#";
+ else
+ out() << " <a href=\"" << linkForNode(node, relative) << '#';
+ out() << registerRef("details") << "\">More...</a>";
+ }
+
+ out() << "</p>\n";
+ generateExtractionMark(node, EndMark);
+ }
+}
+
+/*!
+ Revised for the new doc format.
+ Generates a table of contents beginning at \a node.
+ */
+void HtmlGenerator::generateTableOfContents(const Node *node, CodeMarker *marker,
+ QList<Section> *sections)
+{
+ QList<Atom *> toc;
+ if (node->doc().hasTableOfContents())
+ toc = node->doc().tableOfContents();
+ if (tocDepth == 0 || (toc.isEmpty() && !sections && !node->isModule())) {
+ generateSidebar();
+ return;
+ }
+
+ int sectionNumber = 1;
+ int detailsBase = 0;
+
+ // disable nested links in table of contents
+ m_inContents = true;
+
+ out() << "<div class=\"sidebar\">\n";
+ out() << "<div class=\"toc\">\n";
+ out() << "<h3 id=\"toc\">Contents</h3>\n";
+
+ if (node->isModule()) {
+ openUnorderedList();
+ if (!static_cast<const CollectionNode *>(node)->noAutoList()) {
+ if (node->hasNamespaces()) {
+ out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#"
+ << registerRef("namespaces") << "\">Namespaces</a></li>\n";
+ }
+ if (node->hasClasses()) {
+ out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#"
+ << registerRef("classes") << "\">Classes</a></li>\n";
+ }
+ }
+ out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#" << registerRef("details")
+ << "\">Detailed Description</a></li>\n";
+ for (const auto &entry : std::as_const(toc)) {
+ if (entry->string().toInt() == 1) {
+ detailsBase = 1;
+ break;
+ }
+ }
+ } else if (sections && (node->isClassNode() || node->isNamespace() || node->isQmlType())) {
+ for (const auto &section : std::as_const(*sections)) {
+ if (!section.members().isEmpty()) {
+ openUnorderedList();
+ out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#"
+ << registerRef(section.plural()) << "\">" << section.title() << "</a></li>\n";
+ }
+ if (!section.reimplementedMembers().isEmpty()) {
+ openUnorderedList();
+ QString ref = QString("Reimplemented ") + section.plural();
+ out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#"
+ << registerRef(ref.toLower()) << "\">"
+ << QString("Reimplemented ") + section.title() << "</a></li>\n";
+ }
+ }
+ if (!node->isNamespace() || node->hasDoc()) {
+ openUnorderedList();
+ out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#"
+ << registerRef("details") << "\">Detailed Description</a></li>\n";
+ }
+ for (const auto &entry : toc) {
+ if (entry->string().toInt() == 1) {
+ detailsBase = 1;
+ break;
+ }
+ }
+ }
+
+ for (const auto &atom : toc) {
+ sectionNumber = atom->string().toInt() + detailsBase;
+ // restrict the ToC depth to the one set by the HTML.tocdepth variable or
+ // print all levels if tocDepth is not set.
+ if (sectionNumber <= tocDepth || tocDepth < 0) {
+ openUnorderedList();
+ int numAtoms;
+ Text headingText = Text::sectionHeading(atom);
+ out() << "<li class=\"level" << sectionNumber << "\">";
+ out() << "<a href=\"" << '#' << Tree::refForAtom(atom) << "\">";
+ generateAtomList(headingText.firstAtom(), node, marker, true, numAtoms);
+ out() << "</a></li>\n";
+ }
+ }
+ closeUnorderedList();
+ out() << "</div>\n";
+ out() << R"(<div class="sidebar-content" id="sidebar-content"></div>)";
+ out() << "</div>\n";
+ m_inContents = false;
+ m_inLink = false;
+}
+
+/*!
+ Outputs a placeholder div where the style can add customized sidebar content.
+ */
+void HtmlGenerator::generateSidebar()
+{
+ out() << "<div class=\"sidebar\">";
+ out() << R"(<div class="sidebar-content" id="sidebar-content"></div>)";
+ out() << "</div>\n";
+}
+
+QString HtmlGenerator::generateAllMembersFile(const Section &section, CodeMarker *marker)
+{
+ if (section.isEmpty())
+ return QString();
+
+ const Aggregate *aggregate = section.aggregate();
+ QString fileName = fileBase(aggregate) + "-members." + fileExtension();
+ beginSubPage(aggregate, fileName);
+ QString title = "List of All Members for " + aggregate->name();
+ generateHeader(title, aggregate, marker);
+ generateSidebar();
+ generateTitle(title, Text(), SmallSubTitle, aggregate, marker);
+ out() << "<p>This is the complete list of members for ";
+ generateFullName(aggregate, nullptr);
+ out() << ", including inherited members.</p>\n";
+
+ generateSectionList(section, aggregate, marker);
+
+ generateFooter();
+ endSubPage();
+ return fileName;
+}
+
+/*!
+ This function creates an html page on which are listed all
+ the members of the QML class used to generte the \a sections,
+ including the inherited members. The \a marker is used for
+ formatting stuff.
+ */
+QString HtmlGenerator::generateAllQmlMembersFile(const Sections &sections, CodeMarker *marker)
+{
+
+ if (sections.allMembersSection().isEmpty())
+ return QString();
+
+ const Aggregate *aggregate = sections.aggregate();
+ QString fileName = fileBase(aggregate) + "-members." + fileExtension();
+ beginSubPage(aggregate, fileName);
+ QString title = "List of All Members for " + aggregate->name();
+ generateHeader(title, aggregate, marker);
+ generateSidebar();
+ generateTitle(title, Text(), SmallSubTitle, aggregate, marker);
+ out() << "<p>This is the complete list of members for ";
+ generateFullName(aggregate, nullptr);
+ out() << ", including inherited members.</p>\n";
+
+ ClassNodesList &cknl = sections.allMembersSection().classNodesList();
+ for (int i = 0; i < cknl.size(); i++) {
+ ClassNodes ckn = cknl[i];
+ const QmlTypeNode *qcn = ckn.first;
+ NodeVector &nodes = ckn.second;
+ if (nodes.isEmpty())
+ continue;
+ if (i != 0) {
+ out() << "<p>The following members are inherited from ";
+ generateFullName(qcn, nullptr);
+ out() << ".</p>\n";
+ }
+ openUnorderedList();
+ for (int j = 0; j < nodes.size(); j++) {
+ Node *node = nodes[j];
+ if (node->access() == Access::Private || node->isInternal())
+ continue;
+ if (node->isSharingComment() && node->sharedCommentNode()->isPropertyGroup())
+ continue;
+
+ std::function<void(Node *)> generate = [&](Node *n) {
+ out() << "<li class=\"fn\" translate=\"no\">";
+ generateQmlItem(n, aggregate, marker, true);
+ if (n->isDefault())
+ out() << " [default]";
+ else if (n->isAttached())
+ out() << " [attached]";
+ // Indent property group members
+ if (n->isPropertyGroup()) {
+ out() << "<ul>\n";
+ const QList<Node *> &collective =
+ static_cast<SharedCommentNode *>(n)->collective();
+ std::for_each(collective.begin(), collective.end(), generate);
+ out() << "</ul>\n";
+ }
+ out() << "</li>\n";
+ };
+ generate(node);
+ }
+ closeUnorderedList();
+ }
+
+
+ generateFooter();
+ endSubPage();
+ return fileName;
+}
+
+QString HtmlGenerator::generateObsoleteMembersFile(const Sections &sections, CodeMarker *marker)
+{
+ SectionPtrVector summary_spv;
+ SectionPtrVector details_spv;
+ if (!sections.hasObsoleteMembers(&summary_spv, &details_spv))
+ return QString();
+
+ Aggregate *aggregate = sections.aggregate();
+ QString title = "Obsolete Members for " + aggregate->name();
+ QString fileName = fileBase(aggregate) + "-obsolete." + fileExtension();
+
+ beginSubPage(aggregate, fileName);
+ generateHeader(title, aggregate, marker);
+ generateSidebar();
+ generateTitle(title, Text(), SmallSubTitle, aggregate, marker);
+
+ out() << "<p><b>The following members of class "
+ << "<a href=\"" << linkForNode(aggregate, nullptr) << "\" translate=\"no\">"
+ << protectEnc(aggregate->name()) << "</a>"
+ << " are deprecated.</b> "
+ << "They are provided to keep old source code working. "
+ << "We strongly advise against using them in new code.</p>\n";
+
+ for (const auto &section : summary_spv) {
+ out() << "<h2>" << protectEnc(section->title()) << "</h2>\n";
+ generateSectionList(*section, aggregate, marker, true);
+ }
+
+ for (const auto &section : details_spv) {
+ out() << "<h2>" << protectEnc(section->title()) << "</h2>\n";
+
+ const NodeVector &members = section->obsoleteMembers();
+ for (const auto &member : members) {
+ if (member->access() != Access::Private)
+ generateDetailedMember(member, aggregate, marker);
+ }
+ }
+
+ generateFooter();
+ endSubPage();
+ return fileName;
+}
+
+/*!
+ Generates a separate file where deprecated members of the QML
+ type \a qcn are listed. The \a marker is used to generate
+ the section lists, which are then traversed and output here.
+ */
+QString HtmlGenerator::generateObsoleteQmlMembersFile(const Sections &sections, CodeMarker *marker)
+{
+ SectionPtrVector summary_spv;
+ SectionPtrVector details_spv;
+ if (!sections.hasObsoleteMembers(&summary_spv, &details_spv))
+ return QString();
+
+ Aggregate *aggregate = sections.aggregate();
+ QString title = "Obsolete Members for " + aggregate->name();
+ QString fileName = fileBase(aggregate) + "-obsolete." + fileExtension();
+
+ beginSubPage(aggregate, fileName);
+ generateHeader(title, aggregate, marker);
+ generateSidebar();
+ generateTitle(title, Text(), SmallSubTitle, aggregate, marker);
+
+ out() << "<p><b>The following members of QML type "
+ << "<a href=\"" << linkForNode(aggregate, nullptr) << "\">"
+ << protectEnc(aggregate->name()) << "</a>"
+ << " are deprecated.</b> "
+ << "They are provided to keep old source code working. "
+ << "We strongly advise against using them in new code.</p>\n";
+
+ for (const auto &section : summary_spv) {
+ QString ref = registerRef(section->title().toLower());
+ out() << "<h2 id=\"" << ref << "\">" << protectEnc(section->title()) << "</h2>\n";
+ generateQmlSummary(section->obsoleteMembers(), aggregate, marker);
+ }
+
+ for (const auto &section : details_spv) {
+ out() << "<h2>" << protectEnc(section->title()) << "</h2>\n";
+ const NodeVector &members = section->obsoleteMembers();
+ for (const auto &member : members) {
+ generateDetailedQmlMember(member, aggregate, marker);
+ out() << "<br/>\n";
+ }
+ }
+
+ generateFooter();
+ endSubPage();
+ return fileName;
+}
+
+void HtmlGenerator::generateClassHierarchy(const Node *relative, NodeMultiMap &classMap)
+{
+ if (classMap.isEmpty())
+ return;
+
+ NodeMap topLevel;
+ for (const auto &it : classMap) {
+ auto *classe = static_cast<ClassNode *>(it);
+ if (classe->baseClasses().isEmpty())
+ topLevel.insert(classe->name(), classe);
+ }
+
+ QStack<NodeMap> stack;
+ stack.push(topLevel);
+
+ out() << "<ul>\n";
+ while (!stack.isEmpty()) {
+ if (stack.top().isEmpty()) {
+ stack.pop();
+ out() << "</ul>\n";
+ } else {
+ ClassNode *child = static_cast<ClassNode *>(*stack.top().begin());
+ out() << "<li>";
+ generateFullName(child, relative);
+ out() << "</li>\n";
+ stack.top().erase(stack.top().begin());
+
+ NodeMap newTop;
+ const auto derivedClasses = child->derivedClasses();
+ for (const RelatedClass &d : derivedClasses) {
+ if (d.m_node && d.m_node->isInAPI())
+ newTop.insert(d.m_node->name(), d.m_node);
+ }
+ if (!newTop.isEmpty()) {
+ stack.push(newTop);
+ out() << "<ul>\n";
+ }
+ }
+ }
+}
+
+/*!
+ Outputs an annotated list of the nodes in \a unsortedNodes.
+ A two-column table is output.
+ */
+void HtmlGenerator::generateAnnotatedList(const Node *relative, CodeMarker *marker,
+ const NodeList &unsortedNodes)
+{
+ if (unsortedNodes.isEmpty() || relative == nullptr)
+ return;
+
+ NodeMultiMap nmm;
+ bool allInternal = true;
+ for (auto *node : unsortedNodes) {
+ if (!node->isInternal() && !node->isDeprecated()) {
+ allInternal = false;
+ nmm.insert(node->fullName(relative), node);
+ }
+ }
+ if (allInternal)
+ return;
+ out() << "<div class=\"table\"><table class=\"annotated\">\n";
+ int row = 0;
+ NodeList nodes = nmm.values();
+ std::sort(nodes.begin(), nodes.end(), Node::nodeNameLessThan);
+
+ for (const auto *node : std::as_const(nodes)) {
+ if (++row % 2 == 1)
+ out() << "<tr class=\"odd topAlign\">";
+ else
+ out() << "<tr class=\"even topAlign\">";
+ out() << "<td class=\"tblName\" translate=\"no\"><p>";
+ generateFullName(node, relative);
+ out() << "</p></td>";
+
+ if (!node->isTextPageNode()) {
+ Text brief = node->doc().trimmedBriefText(node->name());
+ if (!brief.isEmpty()) {
+ out() << "<td class=\"tblDescr\"><p>";
+ generateText(brief, node, marker);
+ out() << "</p></td>";
+ } else if (!node->reconstitutedBrief().isEmpty()) {
+ out() << "<td class=\"tblDescr\"><p>";
+ out() << node->reconstitutedBrief();
+ out() << "</p></td>";
+ }
+ } else {
+ out() << "<td class=\"tblDescr\"><p>";
+ if (!node->reconstitutedBrief().isEmpty()) {
+ out() << node->reconstitutedBrief();
+ } else
+ out() << protectEnc(node->doc().briefText().toString());
+ out() << "</p></td>";
+ }
+ out() << "</tr>\n";
+ }
+ out() << "</table></div>\n";
+}
+
+/*!
+ Outputs a series of annotated lists from the nodes in \a nmm,
+ divided into sections based by the key names in the multimap.
+ */
+void HtmlGenerator::generateAnnotatedLists(const Node *relative, CodeMarker *marker,
+ const NodeMultiMap &nmm)
+{
+ const auto &uniqueKeys = nmm.uniqueKeys();
+ for (const QString &name : uniqueKeys) {
+ if (!name.isEmpty()) {
+ out() << "<h2 id=\"" << registerRef(name.toLower()) << "\">" << protectEnc(name)
+ << "</h2>\n";
+ }
+ generateAnnotatedList(relative, marker, nmm.values(name));
+ }
+}
+
+/*!
+ This function finds the common prefix of the names of all
+ the classes in the class map \a nmm and then generates a
+ compact list of the class names alphabetized on the part
+ of the name not including the common prefix. You can tell
+ the function to use \a commonPrefix as the common prefix,
+ but normally you let it figure it out itself by looking at
+ the name of the first and last classes in the class map
+ \a nmm.
+ */
+void HtmlGenerator::generateCompactList(ListType listType, const Node *relative,
+ const NodeMultiMap &nmm, bool includeAlphabet,
+ const QString &commonPrefix)
+{
+ if (nmm.isEmpty())
+ return;
+
+ const int NumParagraphs = 37; // '0' to '9', 'A' to 'Z', '_'
+ qsizetype commonPrefixLen = commonPrefix.size();
+
+ /*
+ Divide the data into 37 paragraphs: 0, ..., 9, A, ..., Z,
+ underscore (_). QAccel will fall in paragraph 10 (A) and
+ QXtWidget in paragraph 33 (X). This is the only place where we
+ assume that NumParagraphs is 37. Each paragraph is a NodeMultiMap.
+ */
+ NodeMultiMap paragraph[NumParagraphs + 1];
+ QString paragraphName[NumParagraphs + 1];
+ QSet<char> usedParagraphNames;
+
+ for (auto c = nmm.constBegin(); c != nmm.constEnd(); ++c) {
+ QStringList pieces = c.key().split("::");
+ int idx = commonPrefixLen;
+ if (idx > 0 && !pieces.last().startsWith(commonPrefix, Qt::CaseInsensitive))
+ idx = 0;
+ QString last = pieces.last().toLower();
+ QString key = last.mid(idx);
+
+ int paragraphNr = NumParagraphs - 1;
+
+ if (key[0].digitValue() != -1) {
+ paragraphNr = key[0].digitValue();
+ } else if (key[0] >= QLatin1Char('a') && key[0] <= QLatin1Char('z')) {
+ paragraphNr = 10 + key[0].unicode() - 'a';
+ }
+
+ paragraphName[paragraphNr] = key[0].toUpper();
+ usedParagraphNames.insert(key[0].toLower().cell());
+ paragraph[paragraphNr].insert(last, c.value());
+ }
+
+ /*
+ Each paragraph j has a size: paragraph[j].count(). In the
+ discussion, we will assume paragraphs 0 to 5 will have sizes
+ 3, 1, 4, 1, 5, 9.
+
+ We now want to compute the paragraph offset. Paragraphs 0 to 6
+ start at offsets 0, 3, 4, 8, 9, 14, 23.
+ */
+ qsizetype paragraphOffset[NumParagraphs + 1]; // 37 + 1
+ paragraphOffset[0] = 0;
+ for (int i = 0; i < NumParagraphs; i++) // i = 0..36
+ paragraphOffset[i + 1] = paragraphOffset[i] + paragraph[i].size();
+
+ /*
+ Output the alphabet as a row of links.
+ */
+ if (includeAlphabet) {
+ out() << "<p class=\"centerAlign functionIndex\" translate=\"no\"><b>";
+ for (int i = 0; i < 26; i++) {
+ QChar ch('a' + i);
+ if (usedParagraphNames.contains(char('a' + i)))
+ out() << QString("<a href=\"#%1\">%2</a>&nbsp;").arg(ch).arg(ch.toUpper());
+ }
+ out() << "</b></p>\n";
+ }
+
+ /*
+ Output a <div> element to contain all the <dl> elements.
+ */
+ out() << "<div class=\"flowListDiv\" translate=\"no\">\n";
+ m_numTableRows = 0;
+
+ int curParNr = 0;
+ int curParOffset = 0;
+ QString previousName;
+ bool multipleOccurrences = false;
+
+ for (int i = 0; i < nmm.size(); i++) {
+ while ((curParNr < NumParagraphs) && (curParOffset == paragraph[curParNr].size())) {
+ ++curParNr;
+ curParOffset = 0;
+ }
+
+ /*
+ Starting a new paragraph means starting a new <dl>.
+ */
+ if (curParOffset == 0) {
+ if (i > 0)
+ out() << "</dl>\n";
+ if (++m_numTableRows % 2 == 1)
+ out() << "<dl class=\"flowList odd\">";
+ else
+ out() << "<dl class=\"flowList even\">";
+ out() << "<dt class=\"alphaChar\"";
+ if (includeAlphabet)
+ out() << QString(" id=\"%1\"").arg(paragraphName[curParNr][0].toLower());
+ out() << "><b>" << paragraphName[curParNr] << "</b></dt>\n";
+ }
+
+ /*
+ Output a <dd> for the current offset in the current paragraph.
+ */
+ out() << "<dd>";
+ if ((curParNr < NumParagraphs) && !paragraphName[curParNr].isEmpty()) {
+ NodeMultiMap::Iterator it;
+ NodeMultiMap::Iterator next;
+ it = paragraph[curParNr].begin();
+ for (int j = 0; j < curParOffset; j++)
+ ++it;
+
+ if (listType == Generic) {
+ /*
+ Previously, we used generateFullName() for this, but we
+ require some special formatting.
+ */
+ out() << "<a href=\"" << linkForNode(it.value(), relative) << "\">";
+ } else if (listType == Obsolete) {
+ QString fileName = fileBase(it.value()) + "-obsolete." + fileExtension();
+ QString link;
+ if (useOutputSubdirs())
+ link = "../%1/"_L1.arg(it.value()->tree()->physicalModuleName());
+ link += fileName;
+ out() << "<a href=\"" << link << "\">";
+ }
+
+ QStringList pieces;
+ if (it.value()->isQmlType()) {
+ QString name = it.value()->name();
+ next = it;
+ ++next;
+ if (name != previousName)
+ multipleOccurrences = false;
+ if ((next != paragraph[curParNr].end()) && (name == next.value()->name())) {
+ multipleOccurrences = true;
+ previousName = name;
+ }
+ if (multipleOccurrences)
+ name += ": " + it.value()->tree()->camelCaseModuleName();
+ pieces << name;
+ } else
+ pieces = it.value()->fullName(relative).split("::");
+ out() << protectEnc(pieces.last());
+ out() << "</a>";
+ if (pieces.size() > 1) {
+ out() << " (";
+ generateFullName(it.value()->parent(), relative);
+ out() << ')';
+ }
+ }
+ out() << "</dd>\n";
+ curParOffset++;
+ }
+ if (nmm.size() > 0)
+ out() << "</dl>\n";
+
+ out() << "</div>\n";
+}
+
+void HtmlGenerator::generateFunctionIndex(const Node *relative)
+{
+ out() << "<p class=\"centerAlign functionIndex\" translate=\"no\"><b>";
+ for (int i = 0; i < 26; i++) {
+ QChar ch('a' + i);
+ out() << QString("<a href=\"#%1\">%2</a>&nbsp;").arg(ch).arg(ch.toUpper());
+ }
+ out() << "</b></p>\n";
+
+ char nextLetter = 'a';
+
+ out() << "<ul translate=\"no\">\n";
+ NodeMapMap &funcIndex = m_qdb->getFunctionIndex();
+ for (auto fnMap = funcIndex.constBegin(); fnMap != funcIndex.constEnd(); ++fnMap) {
+ const QString &key = fnMap.key();
+ const QChar firstLetter = key.isEmpty() ? QChar('A') : key.front();
+ Q_ASSERT_X(firstLetter.unicode() < 256, "generateFunctionIndex",
+ "Only valid C++ identifiers were expected");
+ const char currentLetter = firstLetter.isLower() ? firstLetter.unicode() : nextLetter - 1;
+
+ if (currentLetter < nextLetter) {
+ out() << "<li>";
+ } else {
+ // TODO: This is not covered by our tests
+ while (nextLetter < currentLetter)
+ out() << QStringLiteral("<li id=\"%1\"></li>").arg(nextLetter++);
+ Q_ASSERT(nextLetter == currentLetter);
+ out() << QStringLiteral("<li id=\"%1\">").arg(nextLetter++);
+ }
+ out() << protectEnc(key) << ':';
+
+ for (auto it = (*fnMap).constBegin(); it != (*fnMap).constEnd(); ++it) {
+ out() << ' ';
+ generateFullName((*it)->parent(), relative, *it);
+ }
+ out() << "</li>\n";
+ }
+ while (nextLetter <= 'z')
+ out() << QStringLiteral("<li id=\"%1\"></li>").arg(nextLetter++);
+ out() << "</ul>\n";
+}
+
+void HtmlGenerator::generateLegaleseList(const Node *relative, CodeMarker *marker)
+{
+ TextToNodeMap &legaleseTexts = m_qdb->getLegaleseTexts();
+ for (auto it = legaleseTexts.cbegin(), end = legaleseTexts.cend(); it != end; ++it) {
+ Text text = it.key();
+ generateText(text, relative, marker);
+ out() << "<ul>\n";
+ do {
+ out() << "<li>";
+ generateFullName(it.value(), relative);
+ out() << "</li>\n";
+ ++it;
+ } while (it != legaleseTexts.constEnd() && it.key() == text);
+ out() << "</ul>\n";
+ }
+}
+
+void HtmlGenerator::generateQmlItem(const Node *node, const Node *relative, CodeMarker *marker,
+ bool summary)
+{
+ QString marked = marker->markedUpQmlItem(node, summary);
+ marked.replace("@param>", "i>");
+
+ marked.replace("<@extra>", "<code class=\"%1 extra\" translate=\"no\">"_L1
+ .arg(summary ? "summary"_L1 : "details"_L1));
+ marked.replace("</@extra>", "</code>");
+
+
+ if (summary) {
+ marked.remove("<@name>");
+ marked.remove("</@name>");
+ marked.remove("<@type>");
+ marked.remove("</@type>");
+ }
+ out() << highlightedCode(marked, relative, false, Node::QML);
+}
+
+/*!
+ This function generates a simple unordered list for the members
+ of collection node \a {cn}. Returns \c true if the list was
+ generated (collection has members), \c false otherwise.
+ */
+bool HtmlGenerator::generateGroupList(CollectionNode *cn)
+{
+ m_qdb->mergeCollections(cn);
+ if (cn->members().isEmpty())
+ return false;
+
+ NodeList members{cn->members()};
+ std::sort(members.begin(), members.end(), Node::nodeNameLessThan);
+ out() << "<ul>\n";
+ for (const auto *node : std::as_const(members)) {
+ out() << "<li translate=\"no\">";
+ generateFullName(node, nullptr);
+ out() << "</li>\n";
+ }
+ out() << "</ul>\n";
+ return true;
+}
+
+void HtmlGenerator::generateList(const Node *relative, CodeMarker *marker, const QString &selector)
+{
+ CNMap cnm;
+ Node::NodeType type = Node::NoType;
+ if (selector == QLatin1String("overviews"))
+ type = Node::Group;
+ else if (selector == QLatin1String("cpp-modules"))
+ type = Node::Module;
+ else if (selector == QLatin1String("qml-modules"))
+ type = Node::QmlModule;
+ if (type != Node::NoType) {
+ NodeList nodeList;
+ m_qdb->mergeCollections(type, cnm, relative);
+ const auto collectionList = cnm.values();
+ nodeList.reserve(collectionList.size());
+ for (auto *collectionNode : collectionList)
+ nodeList.append(collectionNode);
+ generateAnnotatedList(relative, marker, nodeList);
+ } else {
+ /*
+ \generatelist {selector} is only allowed in a
+ comment where the topic is \group, \module, or
+ \qmlmodule.
+ */
+ if (relative && !relative->isCollectionNode()) {
+ relative->doc().location().warning(
+ QStringLiteral("\\generatelist {%1} is only allowed in \\group, "
+ "\\module and \\qmlmodule comments.")
+ .arg(selector));
+ return;
+ }
+ auto *node = const_cast<Node *>(relative);
+ auto *collectionNode = static_cast<CollectionNode *>(node);
+ m_qdb->mergeCollections(collectionNode);
+ generateAnnotatedList(collectionNode, marker, collectionNode->members());
+ }
+}
+
+void HtmlGenerator::generateSection(const NodeVector &nv, const Node *relative, CodeMarker *marker)
+{
+ bool alignNames = true;
+ if (!nv.isEmpty()) {
+ bool twoColumn = false;
+ if (nv.first()->isProperty()) {
+ twoColumn = (nv.size() >= 5);
+ alignNames = false;
+ }
+ if (alignNames) {
+ out() << "<div class=\"table\"><table class=\"alignedsummary\" translate=\"no\">\n";
+ } else {
+ if (twoColumn)
+ out() << "<div class=\"table\"><table class=\"propsummary\" translate=\"no\">\n"
+ << "<tr><td class=\"topAlign\">";
+ out() << "<ul>\n";
+ }
+
+ int i = 0;
+ for (const auto &member : nv) {
+ if (member->access() == Access::Private)
+ continue;
+
+ if (alignNames) {
+ out() << "<tr><td class=\"memItemLeft rightAlign topAlign\"> ";
+ } else {
+ if (twoColumn && i == (nv.size() + 1) / 2)
+ out() << "</ul></td><td class=\"topAlign\"><ul>\n";
+ out() << "<li class=\"fn\" translate=\"no\">";
+ }
+
+ generateSynopsis(member, relative, marker, Section::Summary, alignNames);
+ if (alignNames)
+ out() << "</td></tr>\n";
+ else
+ out() << "</li>\n";
+ i++;
+ }
+ if (alignNames)
+ out() << "</table></div>\n";
+ else {
+ out() << "</ul>\n";
+ if (twoColumn)
+ out() << "</td></tr>\n</table></div>\n";
+ }
+ }
+}
+
+void HtmlGenerator::generateSectionList(const Section &section, const Node *relative,
+ CodeMarker *marker, bool useObsoleteMembers)
+{
+ bool alignNames = true;
+ const NodeVector &members =
+ (useObsoleteMembers ? section.obsoleteMembers() : section.members());
+ if (!members.isEmpty()) {
+ bool hasPrivateSignals = false;
+ bool isInvokable = false;
+ bool twoColumn = false;
+ if (section.style() == Section::AllMembers) {
+ alignNames = false;
+ twoColumn = (members.size() >= 16);
+ } else if (members.first()->isProperty()) {
+ twoColumn = (members.size() >= 5);
+ alignNames = false;
+ }
+ if (alignNames) {
+ out() << "<div class=\"table\"><table class=\"alignedsummary\" translate=\"no\">\n";
+ } else {
+ if (twoColumn)
+ out() << "<div class=\"table\"><table class=\"propsummary\" translate=\"no\">\n"
+ << "<tr><td class=\"topAlign\">";
+ out() << "<ul>\n";
+ }
+
+ int i = 0;
+ for (const auto &member : members) {
+ if (member->access() == Access::Private)
+ continue;
+
+ if (alignNames) {
+ out() << "<tr><td class=\"memItemLeft topAlign rightAlign\"> ";
+ } else {
+ if (twoColumn && i == (members.size() + 1) / 2)
+ out() << "</ul></td><td class=\"topAlign\"><ul>\n";
+ out() << "<li class=\"fn\" translate=\"no\">";
+ }
+
+ generateSynopsis(member, relative, marker, section.style(), alignNames);
+ if (member->isFunction()) {
+ const auto *fn = static_cast<const FunctionNode *>(member);
+ if (fn->isPrivateSignal()) {
+ hasPrivateSignals = true;
+ if (alignNames)
+ out() << "</td><td class=\"memItemRight bottomAlign\">[see note below]";
+ } else if (fn->isInvokable()) {
+ isInvokable = true;
+ if (alignNames)
+ out() << "</td><td class=\"memItemRight bottomAlign\">[see note below]";
+ }
+ }
+ if (alignNames)
+ out() << "</td></tr>\n";
+ else
+ out() << "</li>\n";
+ i++;
+ }
+ if (alignNames)
+ out() << "</table></div>\n";
+ else {
+ out() << "</ul>\n";
+ if (twoColumn)
+ out() << "</td></tr>\n</table></div>\n";
+ }
+ if (alignNames) {
+ if (hasPrivateSignals)
+ generateAddendum(relative, Generator::PrivateSignal, marker);
+ if (isInvokable)
+ generateAddendum(relative, Generator::Invokable, marker);
+ }
+ }
+
+ if (!useObsoleteMembers && section.style() == Section::Summary
+ && !section.inheritedMembers().isEmpty()) {
+ out() << "<ul>\n";
+ generateSectionInheritedList(section, relative);
+ out() << "</ul>\n";
+ }
+}
+
+void HtmlGenerator::generateSectionInheritedList(const Section &section, const Node *relative)
+{
+ const QList<std::pair<Aggregate *, int>> &inheritedMembers = section.inheritedMembers();
+ for (const auto &member : inheritedMembers) {
+ out() << "<li class=\"fn\" translate=\"no\">";
+ out() << member.second << ' ';
+ if (member.second == 1) {
+ out() << section.singular();
+ } else {
+ out() << section.plural();
+ }
+ out() << " inherited from <a href=\"" << fileName(member.first) << '#'
+ << Generator::cleanRef(section.title().toLower()) << "\">"
+ << protectEnc(member.first->plainFullName(relative)) << "</a></li>\n";
+ }
+}
+
+void HtmlGenerator::generateSynopsis(const Node *node, const Node *relative, CodeMarker *marker,
+ Section::Style style, bool alignNames)
+{
+ QString marked = marker->markedUpSynopsis(node, relative, style);
+ marked.replace("@param>", "i>");
+
+ if (style == Section::Summary) {
+ marked.remove("<@name>");
+ marked.remove("</@name>");
+ }
+
+ if (style == Section::AllMembers) {
+ static const QRegularExpression extraRegExp("<@extra>.*</@extra>",
+ QRegularExpression::InvertedGreedinessOption);
+ marked.remove(extraRegExp);
+ } else {
+ marked.replace("<@extra>", "<code class=\"%1 extra\" translate=\"no\">"_L1
+ .arg(style == Section::Summary ? "summary"_L1 : "details"_L1));
+ marked.replace("</@extra>", "</code>");
+ }
+
+ if (style != Section::Details) {
+ marked.remove("<@type>");
+ marked.remove("</@type>");
+ }
+
+ out() << highlightedCode(marked, relative, alignNames);
+}
+
+QString HtmlGenerator::highlightedCode(const QString &markedCode, const Node *relative,
+ bool alignNames, Node::Genus genus)
+{
+ QString src = markedCode;
+ QString html;
+ html.reserve(src.size());
+ QStringView arg;
+ QStringView par1;
+
+ const QChar charLangle = '<';
+ const QChar charAt = '@';
+
+ static const QString typeTag("type");
+ static const QString headerTag("headerfile");
+ static const QString funcTag("func");
+ static const QString linkTag("link");
+
+ // replace all <@link> tags: "(<@link node=\"([^\"]+)\">).*(</@link>)"
+ // replace all <@func> tags: "(<@func target=\"([^\"]*)\">)(.*)(</@func>)"
+ // replace all "(<@(type|headerfile)(?: +[^>]*)?>)(.*)(</@\\2>)" tags
+ bool done = false;
+ for (int i = 0, srcSize = src.size(); i < srcSize;) {
+ if (src.at(i) == charLangle && src.at(i + 1) == charAt) {
+ if (alignNames && !done) {
+ html += QLatin1String("</td><td class=\"memItemRight bottomAlign\">");
+ done = true;
+ }
+ i += 2;
+ if (parseArg(src, linkTag, &i, srcSize, &arg, &par1)) {
+ html += QLatin1String("<b>");
+ const Node *n = CodeMarker::nodeForString(par1.toString());
+ QString link = linkForNode(n, relative);
+ addLink(link, arg, &html);
+ html += QLatin1String("</b>");
+ } else if (parseArg(src, funcTag, &i, srcSize, &arg, &par1)) {
+ const FunctionNode *fn = m_qdb->findFunctionNode(par1.toString(), relative, genus);
+ QString link = linkForNode(fn, relative);
+ addLink(link, arg, &html);
+ par1 = QStringView();
+ } else if (parseArg(src, typeTag, &i, srcSize, &arg, &par1)) {
+ par1 = QStringView();
+ const Node *n = m_qdb->findTypeNode(arg.toString(), relative, genus);
+ html += QLatin1String("<span class=\"type\">");
+ if (n && (n->isQmlBasicType())) {
+ if (relative && (relative->genus() == n->genus() || genus == n->genus()))
+ addLink(linkForNode(n, relative), arg, &html);
+ else
+ html += arg;
+ } else
+ addLink(linkForNode(n, relative), arg, &html);
+ html += QLatin1String("</span>");
+ } else if (parseArg(src, headerTag, &i, srcSize, &arg, &par1)) {
+ par1 = QStringView();
+ if (arg.startsWith(QLatin1Char('&')))
+ html += arg;
+ else {
+ const Node *n = m_qdb->findNodeForInclude(QStringList(arg.toString()));
+ if (n && n != relative)
+ addLink(linkForNode(n, relative), arg, &html);
+ else
+ html += arg;
+ }
+ } else {
+ html += charLangle;
+ html += charAt;
+ }
+ } else {
+ html += src.at(i++);
+ }
+ }
+
+ // replace all
+ // "<@comment>" -> "<span class=\"comment\">";
+ // "<@preprocessor>" -> "<span class=\"preprocessor\">";
+ // "<@string>" -> "<span class=\"string\">";
+ // "<@char>" -> "<span class=\"char\">";
+ // "<@number>" -> "<span class=\"number\">";
+ // "<@op>" -> "<span class=\"operator\">";
+ // "<@type>" -> "<span class=\"type\">";
+ // "<@name>" -> "<span class=\"name\">";
+ // "<@keyword>" -> "<span class=\"keyword\">";
+ // "</@(?:comment|preprocessor|string|char|number|op|type|name|keyword)>" -> "</span>"
+ src = html;
+ html = QString();
+ html.reserve(src.size());
+ static const QLatin1String spanTags[] = {
+ QLatin1String("comment>"), QLatin1String("<span class=\"comment\">"),
+ QLatin1String("preprocessor>"), QLatin1String("<span class=\"preprocessor\">"),
+ QLatin1String("string>"), QLatin1String("<span class=\"string\">"),
+ QLatin1String("char>"), QLatin1String("<span class=\"char\">"),
+ QLatin1String("number>"), QLatin1String("<span class=\"number\">"),
+ QLatin1String("op>"), QLatin1String("<span class=\"operator\">"),
+ QLatin1String("type>"), QLatin1String("<span class=\"type\">"),
+ QLatin1String("name>"), QLatin1String("<span class=\"name\">"),
+ QLatin1String("keyword>"), QLatin1String("<span class=\"keyword\">")
+ };
+ int nTags = 9;
+ // Update the upper bound of k in the following code to match the length
+ // of the above array.
+ for (int i = 0, n = src.size(); i < n;) {
+ if (src.at(i) == QLatin1Char('<')) {
+ if (src.at(i + 1) == QLatin1Char('@')) {
+ i += 2;
+ bool handled = false;
+ for (int k = 0; k != nTags; ++k) {
+ const QLatin1String &tag = spanTags[2 * k];
+ if (i + tag.size() <= src.size() && tag == QStringView(src).mid(i, tag.size())) {
+ html += spanTags[2 * k + 1];
+ i += tag.size();
+ handled = true;
+ break;
+ }
+ }
+ if (!handled) {
+ // drop 'our' unknown tags (the ones still containing '@')
+ while (i < n && src.at(i) != QLatin1Char('>'))
+ ++i;
+ ++i;
+ }
+ continue;
+ } else if (src.at(i + 1) == QLatin1Char('/') && src.at(i + 2) == QLatin1Char('@')) {
+ i += 3;
+ bool handled = false;
+ for (int k = 0; k != nTags; ++k) {
+ const QLatin1String &tag = spanTags[2 * k];
+ if (i + tag.size() <= src.size() && tag == QStringView(src).mid(i, tag.size())) {
+ html += QLatin1String("</span>");
+ i += tag.size();
+ handled = true;
+ break;
+ }
+ }
+ if (!handled) {
+ // drop 'our' unknown tags (the ones still containing '@')
+ while (i < n && src.at(i) != QLatin1Char('>'))
+ ++i;
+ ++i;
+ }
+ continue;
+ }
+ }
+ html += src.at(i);
+ ++i;
+ }
+ return html;
+}
+
+void HtmlGenerator::generateLink(const Atom *atom)
+{
+ Q_ASSERT(m_inLink);
+
+ if (m_linkNode && m_linkNode->isFunction()) {
+ auto match = XmlGenerator::m_funcLeftParen.match(atom->string());
+ if (match.hasMatch()) {
+ // C++: move () outside of link
+ qsizetype leftParenLoc = match.capturedStart(1);
+ out() << protectEnc(atom->string().left(leftParenLoc));
+ endLink();
+ out() << protectEnc(atom->string().mid(leftParenLoc));
+ return;
+ }
+ }
+ out() << protectEnc(atom->string());
+}
+
+QString HtmlGenerator::protectEnc(const QString &string)
+{
+ return protect(string);
+}
+
+QString HtmlGenerator::protect(const QString &string)
+{
+#define APPEND(x) \
+ if (html.isEmpty()) { \
+ html = string; \
+ html.truncate(i); \
+ } \
+ html += (x);
+
+ QString html;
+ qsizetype n = string.size();
+
+ for (int i = 0; i < n; ++i) {
+ QChar ch = string.at(i);
+
+ if (ch == QLatin1Char('&')) {
+ APPEND("&amp;");
+ } else if (ch == QLatin1Char('<')) {
+ APPEND("&lt;");
+ } else if (ch == QLatin1Char('>')) {
+ APPEND("&gt;");
+ } else if (ch == QChar(8211)) {
+ APPEND("&ndash;");
+ } else if (ch == QChar(8212)) {
+ APPEND("&mdash;");
+ } else if (ch == QLatin1Char('"')) {
+ APPEND("&quot;");
+ } else {
+ if (!html.isEmpty())
+ html += ch;
+ }
+ }
+
+ if (!html.isEmpty())
+ return html;
+ return string;
+
+#undef APPEND
+}
+
+QString HtmlGenerator::fileBase(const Node *node) const
+{
+ QString result = Generator::fileBase(node);
+ if (!node->isAggregate() && node->isDeprecated())
+ result += QLatin1String("-obsolete");
+ return result;
+}
+
+QString HtmlGenerator::fileName(const Node *node)
+{
+ if (node->isExternalPage())
+ return node->name();
+ return Generator::fileName(node);
+}
+
+void HtmlGenerator::generateFullName(const Node *apparentNode, const Node *relative,
+ const Node *actualNode)
+{
+ if (actualNode == nullptr)
+ actualNode = apparentNode;
+ bool link = !linkForNode(actualNode, relative).isEmpty();
+ if (link) {
+ out() << "<a href=\"" << linkForNode(actualNode, relative);
+ if (actualNode->isDeprecated())
+ out() << "\" class=\"obsolete";
+ out() << "\">";
+ }
+ out() << protectEnc(apparentNode->fullName(relative));
+ if (link)
+ out() << "</a>";
+}
+
+void HtmlGenerator::generateDetailedMember(const Node *node, const PageNode *relative,
+ CodeMarker *marker)
+{
+ const EnumNode *etn;
+ generateExtractionMark(node, MemberMark);
+ QString nodeRef = nullptr;
+ if (node->isSharedCommentNode()) {
+ const auto *scn = reinterpret_cast<const SharedCommentNode *>(node);
+ const QList<Node *> &collective = scn->collective();
+ if (collective.size() > 1)
+ out() << "<div class=\"fngroup\">\n";
+ for (const auto *sharedNode : collective) {
+ nodeRef = refForNode(sharedNode);
+ out() << R"(<h3 class="fn fngroupitem" translate="no" id=")" << nodeRef << "\">";
+ generateSynopsis(sharedNode, relative, marker, Section::Details);
+ out() << "</h3>";
+ }
+ if (collective.size() > 1)
+ out() << "</div>";
+ out() << '\n';
+ } else {
+ nodeRef = refForNode(node);
+ if (node->isEnumType() && (etn = static_cast<const EnumNode *>(node))->flagsType()) {
+ out() << R"(<h3 class="flags" id=")" << nodeRef << "\">";
+ generateSynopsis(etn, relative, marker, Section::Details);
+ out() << "<br/>";
+ generateSynopsis(etn->flagsType(), relative, marker, Section::Details);
+ out() << "</h3>\n";
+ } else {
+ out() << R"(<h3 class="fn" translate="no" id=")" << nodeRef << "\">";
+ generateSynopsis(node, relative, marker, Section::Details);
+ out() << "</h3>" << '\n';
+ }
+ }
+
+ generateStatus(node, marker);
+ generateBody(node, marker);
+ generateOverloadedSignal(node, marker);
+ generateComparisonCategory(node, marker);
+ generateThreadSafeness(node, marker);
+ generateSince(node, marker);
+ generateNoexceptNote(node, marker);
+
+ if (node->isProperty()) {
+ const auto property = static_cast<const PropertyNode *>(node);
+ if (property->propertyType() == PropertyNode::PropertyType::StandardProperty) {
+ Section section("", "", "", "", Section::Accessors);
+
+ section.appendMembers(property->getters().toVector());
+ section.appendMembers(property->setters().toVector());
+ section.appendMembers(property->resetters().toVector());
+
+ if (!section.members().isEmpty()) {
+ out() << "<p><b>Access functions:</b></p>\n";
+ generateSectionList(section, node, marker);
+ }
+
+ Section notifiers("", "", "", "", Section::Accessors);
+ notifiers.appendMembers(property->notifiers().toVector());
+
+ if (!notifiers.members().isEmpty()) {
+ out() << "<p><b>Notifier signal:</b></p>\n";
+ generateSectionList(notifiers, node, marker);
+ }
+ }
+ } else if (node->isEnumType()) {
+ const auto *enumTypeNode = static_cast<const EnumNode *>(node);
+ if (enumTypeNode->flagsType()) {
+ out() << "<p>The " << protectEnc(enumTypeNode->flagsType()->name())
+ << " type is a typedef for "
+ << "<a href=\"" << m_qflagsHref << "\">QFlags</a>&lt;"
+ << protectEnc(enumTypeNode->name()) << "&gt;. It stores an OR combination of "
+ << protectEnc(enumTypeNode->name()) << " values.</p>\n";
+ }
+ }
+ generateAlsoList(node, marker);
+ generateExtractionMark(node, EndMark);
+}
+
+/*!
+ This version of the function is called when outputting the link
+ to an example file or example image, where the \a link is known
+ to be correct.
+ */
+void HtmlGenerator::beginLink(const QString &link)
+{
+ m_link = link;
+ m_inLink = true;
+ m_linkNode = nullptr;
+
+ if (!m_link.isEmpty())
+ out() << "<a href=\"" << m_link << "\" translate=\"no\">";
+}
+
+void HtmlGenerator::beginLink(const QString &link, const Node *node, const Node *relative)
+{
+ m_link = link;
+ m_inLink = true;
+ m_linkNode = node;
+ if (m_link.isEmpty())
+ return;
+
+ const QString &translate_attr =
+ (node && node->genus() & Node::API) ? " translate=\"no\""_L1 : ""_L1;
+
+ if (node == nullptr || (relative != nullptr && node->status() == relative->status()))
+ out() << "<a href=\"" << m_link << "\"%1>"_L1.arg(translate_attr);
+ else if (node->isDeprecated())
+ out() << "<a href=\"" << m_link << "\" class=\"obsolete\"%1>"_L1.arg(translate_attr);
+ else
+ out() << "<a href=\"" << m_link << "\"%1>"_L1.arg(translate_attr);
+}
+
+void HtmlGenerator::endLink()
+{
+ if (!m_inLink)
+ return;
+
+ m_inLink = false;
+ m_linkNode = nullptr;
+
+ if (!m_link.isEmpty())
+ out() << "</a>";
+}
+
+/*!
+ Generates the summary list for the \a members. Only used for
+ sections of QML element documentation.
+ */
+void HtmlGenerator::generateQmlSummary(const NodeVector &members, const Node *relative,
+ CodeMarker *marker)
+{
+ if (!members.isEmpty()) {
+ out() << "<ul>\n";
+ for (const auto &member : members) {
+ out() << "<li class=\"fn\" translate=\"no\">";
+ generateQmlItem(member, relative, marker, true);
+ if (member->isPropertyGroup()) {
+ const auto *scn = static_cast<const SharedCommentNode *>(member);
+ if (scn->count() > 0) {
+ out() << "<ul>\n";
+ const QList<Node *> &sharedNodes = scn->collective();
+ for (const auto &node : sharedNodes) {
+ if (node->isQmlProperty()) {
+ out() << "<li class=\"fn\" translate=\"no\">";
+ generateQmlItem(node, relative, marker, true);
+ out() << "</li>\n";
+ }
+ }
+ out() << "</ul>\n";
+ }
+ }
+ out() << "</li>\n";
+ }
+ out() << "</ul>\n";
+ }
+}
+
+/*!
+ Outputs the html detailed documentation for a section
+ on a QML element reference page.
+ */
+void HtmlGenerator::generateDetailedQmlMember(Node *node, const Aggregate *relative,
+ CodeMarker *marker)
+{
+ generateExtractionMark(node, MemberMark);
+
+ QString qmlItemHeader("<div class=\"qmlproto\" translate=\"no\">\n"
+ "<div class=\"table\"><table class=\"qmlname\">\n");
+
+ QString qmlItemStart("<tr valign=\"top\" class=\"odd\" id=\"%1\">\n"
+ "<td class=\"%2\"><p>\n");
+ QString qmlItemEnd("</p></td></tr>\n");
+
+ QString qmlItemFooter("</table></div></div>\n");
+
+ auto generateQmlProperty = [&](Node *n) {
+ out() << qmlItemStart.arg(refForNode(n), "tblQmlPropNode");
+ generateQmlItem(n, relative, marker, false);
+ out() << qmlItemEnd;
+ };
+
+ auto generateQmlMethod = [&](Node *n) {
+ out() << qmlItemStart.arg(refForNode(n), "tblQmlFuncNode");
+ generateSynopsis(n, relative, marker, Section::Details, false);
+ out() << qmlItemEnd;
+ };
+
+ out() << "<div class=\"qmlitem\">";
+ if (node->isPropertyGroup()) {
+ const auto *scn = static_cast<const SharedCommentNode *>(node);
+ out() << qmlItemHeader;
+ if (!scn->name().isEmpty()) {
+ const QString nodeRef = refForNode(scn);
+ out() << R"(<tr valign="top" class="even" id=")" << nodeRef << "\">";
+ out() << "<th class=\"centerAlign\"><p>";
+ out() << "<b>" << scn->name() << " group</b>";
+ out() << "</p></th></tr>\n";
+ }
+ const QList<Node *> sharedNodes = scn->collective();
+ for (const auto &sharedNode : sharedNodes) {
+ if (sharedNode->isQmlProperty())
+ generateQmlProperty(sharedNode);
+ }
+ out() << qmlItemFooter;
+ } else if (node->isQmlProperty()) {
+ out() << qmlItemHeader;
+ generateQmlProperty(node);
+ out() << qmlItemFooter;
+ } else if (node->isSharedCommentNode()) {
+ const auto *scn = reinterpret_cast<const SharedCommentNode *>(node);
+ const QList<Node *> &sharedNodes = scn->collective();
+ if (sharedNodes.size() > 1)
+ out() << "<div class=\"fngroup\">\n";
+ out() << qmlItemHeader;
+ for (const auto &sharedNode : sharedNodes) {
+ // Generate the node only if it's a QML method
+ if (sharedNode->isFunction(Node::QML))
+ generateQmlMethod(sharedNode);
+ else if (sharedNode->isQmlProperty())
+ generateQmlProperty(sharedNode);
+ }
+ out() << qmlItemFooter;
+ if (sharedNodes.size() > 1)
+ out() << "</div>"; // fngroup
+ } else { // assume the node is a method/signal handler
+ out() << qmlItemHeader;
+ generateQmlMethod(node);
+ out() << qmlItemFooter;
+ }
+
+ out() << "<div class=\"qmldoc\">";
+ generateStatus(node, marker);
+ generateBody(node, marker);
+ generateThreadSafeness(node, marker);
+ generateSince(node, marker);
+ generateAlsoList(node, marker);
+ out() << "</div></div>";
+ generateExtractionMark(node, EndMark);
+}
+
+void HtmlGenerator::generateExtractionMark(const Node *node, ExtractionMarkType markType)
+{
+ if (markType != EndMark) {
+ out() << "<!-- $$$" + node->name();
+ if (markType == MemberMark) {
+ if (node->isFunction()) {
+ const auto *func = static_cast<const FunctionNode *>(node);
+ if (!func->hasAssociatedProperties()) {
+ if (func->overloadNumber() == 0)
+ out() << "[overload1]";
+ out() << "$$$" + func->name() + func->parameters().rawSignature().remove(' ');
+ }
+ } else if (node->isProperty()) {
+ out() << "-prop";
+ const auto *prop = static_cast<const PropertyNode *>(node);
+ const NodeList &list = prop->functions();
+ for (const auto *propFuncNode : list) {
+ if (propFuncNode->isFunction()) {
+ const auto *func = static_cast<const FunctionNode *>(propFuncNode);
+ out() << "$$$" + func->name()
+ + func->parameters().rawSignature().remove(' ');
+ }
+ }
+ } else if (node->isEnumType()) {
+ const auto *enumNode = static_cast<const EnumNode *>(node);
+ const auto &items = enumNode->items();
+ for (const auto &item : items)
+ out() << "$$$" + item.name();
+ }
+ } else if (markType == BriefMark) {
+ out() << "-brief";
+ } else if (markType == DetailedDescriptionMark) {
+ out() << "-description";
+ }
+ out() << " -->\n";
+ } else {
+ out() << "<!-- @@@" + node->name() + " -->\n";
+ }
+}
+
+QT_END_NAMESPACE