// 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 #include #include #include #include #include #include #include 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(""); *res += nestedStuff; *res += QLatin1String(""); } else { *res += nestedStuff; } } /*! \internal Convenience method that starts an unordered list if not in one. */ inline void HtmlGenerator::openUnorderedList() { if (!s_inUnorderedList) { out() << "
    \n"; s_inUnorderedList = true; } } /*! \internal Convenience method that closes an unordered list if in one. */ inline void HtmlGenerator::closeUnorderedList() { if (s_inUnorderedList) { out() << "
\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, "", "" }, { ATOM_FORMATTING_INDEX, "" }, { ATOM_FORMATTING_ITALIC, "", "" }, { ATOM_FORMATTING_PARAMETER, "", "" }, { ATOM_FORMATTING_SUBSCRIPT, "", "" }, { ATOM_FORMATTING_SUPERSCRIPT, "", "" }, { ATOM_FORMATTING_TELETYPE, "", "" }, // tag is not supported in HTML5 { ATOM_FORMATTING_UICONTROL, "", "" }, { ATOM_FORMATTING_UNDERLINE, "", "" }, { 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_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() << "

"; rewritePropertyBrief(atom, relative); break; case Atom::BriefRight: if (hasBrief(relative)) out() << "

\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() << "

"; in_para = true; break; case Atom::CaptionRight: endLink(); if (in_para) { out() << "

\n"; in_para = false; } break; case Atom::Qml: out() << "
"
              << trimmedTrailing(highlightedCode(indent(m_codeIndent, atom->string()), relative,
                                                 false, Node::QML),
                                 m_codePrefix, m_codeSuffix)
              << "
\n"; break; case Atom::Code: out() << "
"
              << trimmedTrailing(highlightedCode(indent(m_codeIndent, atom->string()), relative),
                                 m_codePrefix, m_codeSuffix)
              << "
\n"; break; case Atom::CodeBad: out() << "
"
              << trimmedTrailing(protectEnc(plainCode(indent(m_codeIndent, atom->string()))),
                                 m_codePrefix, m_codeSuffix)
              << "
\n"; break; case Atom::DetailsLeft: out() << "
\n"; if (!atom->string().isEmpty()) out() << "" << protectEnc(atom->string()) << "\n"; else out() << "...\n"; break; case Atom::DetailsRight: out() << "
\n"; break; case Atom::DivLeft: out() << "string().isEmpty()) out() << ' ' << atom->string(); out() << '>'; break; case Atom::DivRight: out() << ""; break; case Atom::FootnoteLeft: // ### For now if (in_para) { out() << "

\n"; in_para = false; } 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()]; if (atom->string() == ATOM_FORMATTING_PARAMETER) { if (atom->next() != nullptr && atom->next()->type() == Atom::String) { static const QRegularExpression subscriptRegExp("^([a-z]+)_([0-9n])$"); auto match = subscriptRegExp.match(atom->next()->string()); if (match.hasMatch()) { out() << match.captured(1) << "" << match.captured(2) << ""; skipAhead = 1; } } } break; case Atom::FormattingRight: if (atom->string() == ATOM_FORMATTING_LINK) { endLink(); } else if (atom->string().startsWith("span ")) { out() << ""; } 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) { QString moduleName = atom->string().mid(idx + 8).trimmed(); Node::NodeType type = typeFromString(atom); QDocDatabase *qdb = QDocDatabase::qdocDB(); const CollectionNode *cn = qdb->getCollectionNode(moduleName, type); if (cn) { if (type == Node::Module) { NodeMap m; cn->getMemberClasses(m); if (!m.isEmpty()) { generateAnnotatedList(relative, marker, m.values()); } } else generateAnnotatedList(relative, marker, cn->members()); } } 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(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() << "
    \n"; const QList
    sinceSections = sections.sinceSections(); for (const auto §ion : sinceSections) { if (!section.members().isEmpty()) { out() << "
  • " << "" << section.title() << "
  • \n"; } } out() << "
\n"; int index = 0; for (const auto §ion : sinceSections) { if (!section.members().isEmpty()) { out() << "

" << protectEnc(section.title()) << "

\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 parentmaps; const QList &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) ? "

Class " : "

QML Type "); out() << ""; QStringList pieces = parent->fullName().split("::"); out() << protectEnc(pieces.last()); out() << "" << ":

\n"; generateSection(nv, relative, marker); out() << "
"; } } else if (index == Sections::SinceEnumValues) { out() << "
\n"; const auto map_it = m_qdb->newEnumValueMaps().constFind(atom->string()); for (auto it = map_it->cbegin(); it != map_it->cend(); ++it) { out() << "\n"; } out() << "
enum value " << "" << it.key() << "
\n"; } else { generateSection(section.members(), relative, marker); } } ++index; } } break; case Atom::BR: out() << "
\n"; break; case Atom::HR: out() << "
\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() << "

"; 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() << "[Missing image " << protectEnc(atom->string()) << "]"; } 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() << "\"""; // TODO: [uncentralized-output-directory-structure] m_helpProjectWriter->addExtraFile("images/" + file_name); setImageFileName(relative, "images/" + file_name); } if (atom->type() == Atom::Image) out() << "

"; } 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() << "
\n" << "

"; out() << formattingLeftMap()[ATOM_FORMATTING_BOLD]; out() << admonType << ": "; out() << formattingRightMap()[ATOM_FORMATTING_BOLD]; } break; case Atom::ImportantRight: case Atom::NoteRight: case Atom::WarningRight: out() << "

\n" << "
\n"; break; case Atom::LegaleseLeft: out() << "
"; break; case Atom::LegaleseRight: out() << "
"; break; case Atom::LineBreak: out() << "
"; 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()) { relative->doc().location().warning( QStringLiteral("Can't link to '%1'").arg(atom->string())); } beginLink(link, nullptr, relative); m_linkNode = node; skipAhead = 1; } break; case Atom::ExampleFileLink: { QString link = linkForExampleFile(atom->string()); if (link.isEmpty() && !noLinkErrors()) relative->doc().location().warning( QStringLiteral("Can't link to '%1'").arg(atom->string())); beginLink(link); skipAhead = 1; } break; case Atom::ExampleImageLink: { QString link = atom->string(); if (link.isEmpty() && !noLinkErrors()) relative->doc().location().warning( QStringLiteral("Can't link to '%1'").arg(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() << "

\n"; in_para = false; } if (atom->string() == ATOM_LIST_BULLET) { out() << "
    \n"; } else if (atom->string() == ATOM_LIST_TAG) { out() << "
    \n"; } else if (atom->string() == ATOM_LIST_VALUE) { out() << R"(
    )"; m_threeColumnEnumValueTable = isThreeColumnEnumValueTable(atom); if (m_threeColumnEnumValueTable) { if (++m_numTableRows % 2 == 1) out() << R"()"; else out() << R"()"; out() << ""; // If not in \enum topic, skip the value column if (relative->isEnumType()) out() << ""; out() << "\n"; } else { out() << "\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"(
      )") .arg(olType, atom->next()->string()); } else out() << QString(R"(
        )").arg(olType); } break; case Atom::ListItemNumber: break; case Atom::ListTagLeft: if (atom->string() == ATOM_LIST_TAG) { out() << "
        "; } else { // (atom->string() == ATOM_LIST_VALUE) std::pair pair = getAtomListValue(atom); skipAhead = pair.second; QString t = protectEnc(plainCode(marker->markedUpEnumValue(pair.first, relative))); out() << "
    \n"; } else { out() << "\n"; } break; case Atom::ListRight: if (atom->string() == ATOM_LIST_BULLET) { out() << "\n"; } else if (atom->string() == ATOM_LIST_TAG) { out() << "\n"; } else if (atom->string() == ATOM_LIST_VALUE) { out() << "
    ConstantValueDescription
    ConstantValue
    " << t << ""; if (relative->isEnumType()) { out() << ""; const auto *enume = static_cast(relative); QString itemValue = enume->itemValue(atom->next()->string()); if (itemValue.isEmpty()) out() << '?'; else out() << "" << protectEnc(itemValue) << ""; } } break; case Atom::SinceTagRight: case Atom::ListTagRight: if (atom->string() == ATOM_LIST_TAG) out() << "\n"; break; case Atom::ListItemLeft: if (atom->string() == ATOM_LIST_TAG) { out() << "
    "; } else if (atom->string() == ATOM_LIST_VALUE) { if (m_threeColumnEnumValueTable) { out() << "
    "; if (matchAhead(atom, Atom::ListItemRight)) out() << " "; } } else { out() << "
  • "; } if (matchAhead(atom, Atom::ParaLeft)) skipAhead = 1; break; case Atom::ListItemRight: if (atom->string() == ATOM_LIST_TAG) { out() << "\n"; } else if (atom->string() == ATOM_LIST_VALUE) { out() << "
  • \n"; } else { out() << "\n"; } break; case Atom::Nop: break; case Atom::ParaLeft: out() << "

    "; in_para = true; break; case Atom::ParaRight: endLink(); if (in_para) { out() << "

    \n"; in_para = false; } // if (!matchAhead(atom, Atom::ListItemRight) && !matchAhead(atom, Atom::TableItemRight)) // out() << "

    \n"; break; case Atom::QuotationLeft: out() << "
    "; break; case Atom::QuotationRight: out() << "
    \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() << ""; m_inSectionHeading = true; break; } case Atom::SectionHeadingRight: out() << "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 pair = getTableWidthAttr(atom); QString attr = pair.second; QString width = pair.first; if (in_para) { out() << "

    \n"; in_para = false; } out() << R"(
    \n "; m_numTableRows = 0; } break; case Atom::TableRight: out() << "
    \n"; break; case Atom::TableHeaderLeft: out() << ""; m_inTableHeader = true; break; case Atom::TableHeaderRight: out() << ""; if (matchAhead(atom, Atom::TableHeaderLeft)) { skipAhead = 1; out() << "\n"; } else { out() << "\n"; m_inTableHeader = false; } break; case Atom::TableRowLeft: if (!atom->string().isEmpty()) out() << "string() << '>'; else if (++m_numTableRows % 2 == 1) out() << R"()"; else out() << R"()"; break; case Atom::TableRowRight: out() << "\n"; break; case Atom::TableItemLeft: { if (m_inTableHeader) out() << "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() << ""; else { out() << ""; } if (matchAhead(atom, Atom::ParaLeft)) skipAhead = 1; break; case Atom::TableOfContents: Q_FALLTHROUGH(); case Atom::Keyword: break; case Atom::Target: out() << "string()) << "\">"; break; case Atom::UnhandledFormat: out() << "<Missing HTML>"; break; case Atom::UnknownCommand: out() << R"(\)" << protectEnc(atom->string()) << ""; break; case Atom::CodeQuoteArgument: case Atom::CodeQuoteCommand: 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()) : "" + protectEnc(group->fullTitle()) + ""; }; QString text{}; const QStringList &groups_names{node->groupNames()}; if (groups_names.isEmpty()) return text; std::vector 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::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); QString templateDecl = aggregate->templateDecl(); if (aggregate->isNamespace()) { rawTitle = aggregate->plainName(); fullTitle = aggregate->plainFullName(); title = rawTitle + " Namespace"; ns = static_cast(aggregate); summarySections = §ions.stdSummarySections(); detailsSections = §ions.stdDetailsSections(); } else if (aggregate->isClassNode()) { rawTitle = aggregate->plainName(); fullTitle = aggregate->plainFullName(); title = rawTitle + QLatin1Char(' ') + word; summarySections = §ions.stdCppClassSummarySections(); detailsSections = §ions.stdCppClassDetailsSections(); } else if (aggregate->isHeader()) { title = fullTitle = rawTitle = aggregate->fullTitle(); summarySections = §ions.stdSummarySections(); detailsSections = §ions.stdDetailsSections(); } Text subtitleText; if (rawTitle != fullTitle || !templateDecl.isEmpty()) { if (aggregate->isClassNode()) { if (!templateDecl.isEmpty()) subtitleText << templateDecl + 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() << "

    "; generateText(brief, ns, marker); out() << "

    \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() << "
  • " << "List of all members, including inherited members
  • \n"; } QString obsoleteLink = generateObsoleteMembersFile(sections, marker); if (!obsoleteLink.isEmpty()) { openUnorderedList(); out() << "
  • " << "Deprecated members
  • \n"; } if (QString groups_text{groupReferenceText(aggregate)}; !groups_text.isEmpty()) { openUnorderedList(); out() << "
  • " << groups_text << "
  • \n"; } closeUnorderedList(); generateThreadSafeness(aggregate, marker); bool needOtherSection = false; for (const auto §ion : 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() << "

    " << protectEnc(section.title()) << "

    \n"; generateSection(section.members(), aggregate, marker); } if (!section.reimplementedMembers().isEmpty()) { QString name = QString("Reimplemented ") + section.title(); QString ref = registerRef(name.toLower()); out() << "

    " << protectEnc(name) << "

    \n"; generateSection(section.reimplementedMembers(), aggregate, marker); } if (!section.inheritedMembers().isEmpty()) { out() << "
      \n"; generateSectionInheritedList(section, aggregate); out() << "
    \n"; } } } if (needOtherSection) { out() << "

    Additional Inherited Members

    \n" "
      \n"; for (const auto §ion : std::as_const(*summarySections)) { if (section.members().isEmpty() && !section.inheritedMembers().isEmpty()) generateSectionInheritedList(section, aggregate); } out() << "
    \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() << "
    \n" << "

    " << "Detailed Description" << "

    \n"; generateBody(aggregate, marker); out() << "
    \n"; generateAlsoList(aggregate, marker); generateExtractionMark(aggregate, EndMark); } for (const auto §ion : std::as_const(*detailsSections)) { bool headerGenerated = false; if (section.isEmpty()) continue; const QList &members = section.members(); for (const auto &member : members) { if (member->access() == Access::Private) // ### check necessary? continue; if (!headerGenerated) { if (!section.divClass().isEmpty()) out() << "
    \n"; out() << "

    " << protectEnc(section.title()) << "

    \n"; headerGenerated = true; } if (!member->isClassNode()) generateDetailedMember(member, aggregate, marker); else { out() << "

    class "; generateFullName(member, aggregate); out() << "

    "; generateBrief(member, marker, aggregate); } QStringList names; names << member->name(); if (member->isFunction()) { const auto *func = reinterpret_cast(member); if (func->isSomeCtor() || func->isDtor() || func->overloadNumber() != 0) names.clear(); } else if (member->isProperty()) { const auto *prop = reinterpret_cast(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(member); if (enume->flagsType()) names << enume->flagsType()->name(); const auto &enumItemNameList = enume->doc().enumItemNames(); const auto &omitEnumItemNameList = enume->doc().omitEnumItemNames(); const auto items = QSet(enumItemNameList.cbegin(), enumItemNameList.cend()) - QSet(omitEnumItemNameList.cbegin(), omitEnumItemNameList.cend()); for (const QString &enumName : items) { names << plainCode(marker->markedUpEnumValue(enumName, enume)); } } } if (headerGenerated && !section.divClass().isEmpty()) out() << "
    \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 = §ions.stdSummarySections(); detailsSections = §ions.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() << "

    " << protectEnc(it->title()) << "

    \n"; generateSection(it->members(), aggregate, marker); } } if (!aggregate->doc().isEmpty()) { generateExtractionMark(aggregate, DetailedDescriptionMark); out() << "
    \n" << "

    " << "Detailed Description" << "

    \n"; generateBody(aggregate, marker); out() << "
    \n"; generateAlsoList(aggregate, marker); generateExtractionMark(aggregate, EndMark); } for (const auto §ion : std::as_const(*detailsSections)) { if (section.isEmpty()) continue; if (!section.divClass().isEmpty()) out() << "
    \n"; out() << "

    " << protectEnc(section.title()) << "

    \n"; const QList &members = section.members(); for (const auto &member : members) { if (!member->isPrivate()) { // ### check necessary? if (!member->isClassNode()) generateDetailedMember(member, aggregate, marker); else { out() << "

    class "; generateFullName(member, aggregate); out() << "

    "; generateBrief(member, marker, aggregate); } QStringList names; names << member->name(); if (member->isFunction()) { const auto *func = reinterpret_cast(member); if (func->isSomeCtor() || func->isDtor() || func->overloadNumber() != 0) names.clear(); } else if (member->isEnumType()) { const auto *enume = reinterpret_cast(member); if (enume->flagsType()) names << enume->flagsType()->name(); const auto &enumItemNameList = enume->doc().enumItemNames(); const auto &omitEnumItemNameList = enume->doc().omitEnumItemNames(); const auto items = QSet(enumItemNameList.cbegin(), enumItemNameList.cend()) - QSet(omitEnumItemNameList.cbegin(), omitEnumItemNameList.cend()); for (const QString &enumName : items) names << plainCode(marker->markedUpEnumValue(enumName, enume)); } } } if (!section.divClass().isEmpty()) out() << "
    \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, §ions.stdQmlTypeSummarySections()); marker = CodeMarker::markerForLanguage(QLatin1String("QML")); generateTitle(htmlTitle, Text() << qcn->subtitle(), subTitleSize, qcn, marker); generateBrief(qcn, marker); generateQmlRequisites(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() << "
  • " << "List of all members, including inherited members
  • \n"; } if (!obsoleteLink.isEmpty()) { out() << "
  • " << "Deprecated members
  • \n"; } } if (QString groups_text{groupReferenceText(qcn)}; !groups_text.isEmpty()) { openUnorderedList(); out() << "
  • " << groups_text << "
  • \n"; } closeUnorderedList(); const QList
    &stdQmlTypeSummarySections = sections.stdQmlTypeSummarySections(); for (const auto §ion : stdQmlTypeSummarySections) { if (!section.isEmpty()) { QString ref = registerRef(section.title().toLower()); out() << "

    " << protectEnc(section.title()) << "

    \n"; generateQmlSummary(section.members(), qcn, marker); } } generateExtractionMark(qcn, DetailedDescriptionMark); out() << "

    " << "Detailed Description" << "

    \n"; generateBody(qcn, marker); generateAlsoList(qcn, marker); generateExtractionMark(qcn, EndMark); const QList
    &stdQmlTypeDetailsSections = sections.stdQmlTypeDetailsSections(); for (const auto §ion : stdQmlTypeDetailsSections) { if (!section.isEmpty()) { out() << "

    " << protectEnc(section.title()) << "

    \n"; const QList &members = section.members(); for (const auto member : members) { generateDetailedQmlMember(member, qcn, marker); out() << "
    \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"(
    \n"; generateBody(pn, marker); out() << "
    \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->getMemberNamespaces(nmm); if (!nmm.isEmpty()) { ref = registerRef("namespaces"); out() << "

    Namespaces

    \n"; generateAnnotatedList(cn, marker, nmm.values()); } nmm.clear(); cn->getMemberClasses(nmm); if (!nmm.isEmpty()) { ref = registerRef("classes"); out() << "

    Classes

    \n"; generateAnnotatedList(cn, marker, nmm.values()); } } } if (cn->isModule() && !cn->doc().briefText().isEmpty()) { generateExtractionMark(cn, DetailedDescriptionMark); ref = registerRef("details"); out() << "
    \n"; out() << "

    " << "Detailed Description" << "

    \n"; } else { generateExtractionMark(cn, DetailedDescriptionMark); out() << R"(
    \n"; } generateBody(cn, marker); out() << "
    \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() << "

    "; generateText(brief, cn, marker); out() << "

    \n"; const QList 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(node)}; std::deque 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 in a single group, use that if (navNodes.empty()) { QStringList groups = static_cast(node)->groupNames(); if (groups.size() == 1) { const Node *groupNode = m_qdb->findNodeByNameAndType(QStringList(groups[0]), &Node::isGroup); if (groupNode && !groupNode->title().isEmpty()) navNodes.push_front(groupNode); } } 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() << "\n" << R"(\n"; else out() << "\n"; } void HtmlGenerator::generateHeader(const QString &title, const Node *node, CodeMarker *marker) { out() << "\n"; out() << QString("\n").arg(naturalLanguage); out() << "\n"; out() << " \n"; if (node && !node->doc().location().isEmpty()) out() << "\n"; if (node && !node->doc().briefText().isEmpty()) { out() << " doc().briefText(), node, marker); out() << "\">\n"; } // determine the rest of the 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() << "\n"; // Include style sheet and script links. out() << m_headerStyles; out() << m_headerScripts; if (m_endHeader.isEmpty()) out() << "\n\n"; else out() << m_endHeader; out() << QString(m_postHeader).replace("\\" + COMMAND_VERSION, m_qdb->version()); bool usingTable = m_postHeader.trimmed().endsWith(QLatin1String("")); 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 linkPair; std::pair 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"( \n"; m_navigationLinks += R"("; if (linkPair.first == linkPair.second && !anchorPair.second.isEmpty()) m_navigationLinks += protect(anchorPair.second); else m_navigationLinks += protect(linkPair.second); m_navigationLinks += "\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"( \n"; if (useSeparator) m_navigationLinks += m_navigationSeparator; m_navigationLinks += R"("; if (linkPair.first == linkPair.second && !anchorPair.second.isEmpty()) m_navigationLinks += protect(anchorPair.second); else m_navigationLinks += protect(linkPair.second); m_navigationLinks += "\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"( \n"; } } if (node && !node->links().empty()) out() << "

    \n" << m_navigationLinks << "

    \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() << "

    " << protectEnc(title) << "

    \n"; if (!subtitle.isEmpty()) { out() << ""; else out() << " class=\"subtitle\"" << attribute << ">"; generateText(subtitle, relative, marker); out() << "\n"; } } void HtmlGenerator::generateFooter(const Node *node) { if (node && !node->links().empty()) out() << "

    \n" << m_navigationLinks << "

    \n"; out() << QString(m_footer).replace("\\" + COMMAND_VERSION, m_qdb->version()) << QString(m_address).replace("\\" + COMMAND_VERSION, m_qdb->version()); out() << "\n"; out() << "\n"; } /*! Lists the required imports and includes in a table. The number of rows is known. */ void HtmlGenerator::generateRequisites(Aggregate *aggregate, CodeMarker *marker) { QMap 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(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 &requisites, const QString &headerText, const Aggregate *aggregate, CodeMarker *marker) { out() << "
    )"; } else { out() << "
  • "; } // 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() << "
  • \n"; for (auto it = requisiteOrder.constBegin(); it != requisiteOrder.constEnd(); ++it) { if (requisites.contains(*it)) { out() << "" << "\n"; } } out() << "
    " << *it << ":" " "; if (*it == headerText) out() << requisites.value(*it).toString(); else generateText(requisites.value(*it), aggregate, marker); out() << "
    \n"; } /*! * \internal * Adds inherited by information to the map. */ void HtmlGenerator::addInheritedByToMap(QMap &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 &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 &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 &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()) { text->clear(); const QString qtComponent = "Qt" + QString::number(QT_VERSION_MAJOR); const QString findPackageText = "find_package(" + qtComponent + " REQUIRED COMPONENTS " + cn->qtCMakeComponent() + ")"; const QString targetLinkLibrariesText = "target_link_libraries(mytarget PRIVATE " + qtComponent + "::" + cn->qtCMakeComponent() + ")"; const Atom lineBreak = Atom(Atom::RawString, "
    \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 &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 &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 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 &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::canonicalizeFileName(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 &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 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() << "
    \n"; for (const auto &requisite : requisiteorder) { if (requisites.contains(requisite)) { out() << "" << ""; } } out() << "
    " << requisite << " "; if (requisite == importText) out() << requisites.value(requisite).toString(); else generateText(requisites.value(requisite), qcn, marker); out() << "
    "; } } 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() << "

    "; generateText(brief, node, marker); if (addLink) { if (!relative || node == relative) out() << " More..."; } out() << "

    \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
    *sections) { QList 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() << "
    \n"; out() << "
    \n"; out() << "

    Contents

    \n"; if (node->isModule()) { openUnorderedList(); if (!static_cast(node)->noAutoList()) { if (node->hasNamespaces()) { out() << "
  • Namespaces
  • \n"; } if (node->hasClasses()) { out() << "
  • Classes
  • \n"; } } out() << "
  • Detailed Description
  • \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 §ion : std::as_const(*sections)) { if (!section.members().isEmpty()) { openUnorderedList(); out() << "
  • " << section.title() << "
  • \n"; } if (!section.reimplementedMembers().isEmpty()) { openUnorderedList(); QString ref = QString("Reimplemented ") + section.plural(); out() << "
  • " << QString("Reimplemented ") + section.title() << "
  • \n"; } } if (!node->isNamespace() || node->hasDoc()) { openUnorderedList(); out() << "
  • Detailed Description
  • \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); QString s = headingText.toString(); out() << "
  • "; out() << ""; generateAtomList(headingText.firstAtom(), node, marker, true, numAtoms); out() << "
  • \n"; } } closeUnorderedList(); out() << "
    \n"; out() << R"()"; out() << "
    \n"; m_inContents = false; m_inLink = false; } /*! Outputs a placeholder div where the style can add customized sidebar content. */ void HtmlGenerator::generateSidebar() { out() << "
    "; out() << R"()"; out() << "
    \n"; } QString HtmlGenerator::generateAllMembersFile(const Section §ion, 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() << "

    This is the complete list of members for "; generateFullName(aggregate, nullptr); out() << ", including inherited members.

    \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 §ions, 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() << "

    This is the complete list of members for "; generateFullName(aggregate, nullptr); out() << ", including inherited members.

    \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() << "

    The following members are inherited from "; generateFullName(qcn, nullptr); out() << ".

    \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 generate = [&](Node *n) { out() << "
  • "; generateQmlItem(n, aggregate, marker, true); if (n->isDefault()) out() << " [default]"; else if (n->isAttached()) out() << " [attached]"; // Indent property group members if (n->isPropertyGroup()) { out() << "
      \n"; const QList &collective = static_cast(n)->collective(); std::for_each(collective.begin(), collective.end(), generate); out() << "
    \n"; } out() << "
  • \n"; }; generate(node); } closeUnorderedList(); } generateFooter(); endSubPage(); return fileName; } QString HtmlGenerator::generateObsoleteMembersFile(const Sections §ions, 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(); QString link; if (useOutputSubdirs() && !Generator::outputSubdir().isEmpty()) link = QString("../" + Generator::outputSubdir() + QLatin1Char('/')); link += fileName; aggregate->setObsoleteLink(link); beginSubPage(aggregate, fileName); generateHeader(title, aggregate, marker); generateSidebar(); generateTitle(title, Text(), SmallSubTitle, aggregate, marker); out() << "

    The following members of class " << "" << protectEnc(aggregate->name()) << "" << " are deprecated. " << "They are provided to keep old source code working. " << "We strongly advise against using them in new code.

    \n"; for (const auto §ion : summary_spv) { out() << "

    " << protectEnc(section->title()) << "

    \n"; generateSectionList(*section, aggregate, marker, true); } for (const auto §ion : details_spv) { out() << "

    " << protectEnc(section->title()) << "

    \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 §ions, 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(); QString link; if (useOutputSubdirs() && !Generator::outputSubdir().isEmpty()) link = QString("../" + Generator::outputSubdir() + QLatin1Char('/')); link += fileName; aggregate->setObsoleteLink(link); beginSubPage(aggregate, fileName); generateHeader(title, aggregate, marker); generateSidebar(); generateTitle(title, Text(), SmallSubTitle, aggregate, marker); out() << "

    The following members of QML type " << "" << protectEnc(aggregate->name()) << "" << " are deprecated. " << "They are provided to keep old source code working. " << "We strongly advise against using them in new code.

    \n"; for (const auto §ion : summary_spv) { QString ref = registerRef(section->title().toLower()); out() << "

    " << protectEnc(section->title()) << "

    \n"; generateQmlSummary(section->obsoleteMembers(), aggregate, marker); } for (const auto §ion : details_spv) { out() << "

    " << protectEnc(section->title()) << "

    \n"; const NodeVector &members = section->obsoleteMembers(); for (const auto &member : members) { generateDetailedQmlMember(member, aggregate, marker); out() << "
    \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(it); if (classe->baseClasses().isEmpty()) topLevel.insert(classe->name(), classe); } QStack stack; stack.push(topLevel); out() << "
      \n"; while (!stack.isEmpty()) { if (stack.top().isEmpty()) { stack.pop(); out() << "
    \n"; } else { ClassNode *child = static_cast(*stack.top().begin()); out() << "
  • "; generateFullName(child, relative); out() << "
  • \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() << "
      \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() << "
      \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() << ""; else out() << ""; out() << ""; if (!node->isTextPageNode()) { Text brief = node->doc().trimmedBriefText(node->name()); if (!brief.isEmpty()) { out() << ""; } else if (!node->reconstitutedBrief().isEmpty()) { out() << ""; } } else { out() << ""; } out() << "\n"; } out() << "

      "; generateFullName(node, relative); out() << "

      "; generateText(brief, node, marker); out() << "

      "; out() << node->reconstitutedBrief(); out() << "

      "; if (!node->reconstitutedBrief().isEmpty()) { out() << node->reconstitutedBrief(); } else out() << protectEnc(node->doc().briefText().toString()); out() << "

      \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() << "

      " << protectEnc(name) << "

      \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 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() << "

      "; for (int i = 0; i < 26; i++) { QChar ch('a' + i); if (usedParagraphNames.contains(char('a' + i))) out() << QString("%2 ").arg(ch).arg(ch.toUpper()); } out() << "

      \n"; } /* Output a
      element to contain all the
      elements. */ out() << "
      \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
      . */ if (curParOffset == 0) { if (i > 0) out() << "
      \n"; if (++m_numTableRows % 2 == 1) out() << "
      "; else out() << "
      "; out() << "
      " << paragraphName[curParNr] << "
      \n"; } /* Output a
      for the current offset in the current paragraph. */ out() << "
      "; 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() << ""; } else if (listType == Obsolete) { QString fileName = fileBase(it.value()) + "-obsolete." + fileExtension(); QString link; if (useOutputSubdirs()) { link = QString("../" + it.value()->outputSubdirectory() + QLatin1Char('/')); } link += fileName; out() << ""; } 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() << ""; if (pieces.size() > 1) { out() << " ("; generateFullName(it.value()->parent(), relative); out() << ')'; } } out() << "
      \n"; curParOffset++; } if (nmm.size() > 0) out() << "
      \n"; out() << "
      \n"; } void HtmlGenerator::generateFunctionIndex(const Node *relative) { out() << "

      "; for (int i = 0; i < 26; i++) { QChar ch('a' + i); out() << QString("%2 ").arg(ch).arg(ch.toUpper()); } out() << "

      \n"; char nextLetter = 'a'; out() << "
        \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() << "
      • "; } else { // TODO: This is not covered by our tests while (nextLetter < currentLetter) out() << QStringLiteral("
      • ").arg(nextLetter++); Q_ASSERT(nextLetter == currentLetter); out() << QStringLiteral("
      • ").arg(nextLetter++); } out() << protectEnc(key) << ':'; for (auto it = (*fnMap).constBegin(); it != (*fnMap).constEnd(); ++it) { out() << ' '; generateFullName((*it)->parent(), relative, *it); } out() << "
      • \n"; } while (nextLetter <= 'z') out() << QStringLiteral("
      • ").arg(nextLetter++); out() << "
      \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() << "
        \n"; do { out() << "
      • "; generateFullName(it.value(), relative); out() << "
      • \n"; ++it; } while (it != legaleseTexts.constEnd() && it.key() == text); out() << "
      \n"; } } void HtmlGenerator::generateQmlItem(const Node *node, const Node *relative, CodeMarker *marker, bool summary) { QString marked = marker->markedUpQmlItem(node, summary); static const QRegularExpression templateTag("(<[^@>]*>)"); auto match = templateTag.match(marked); if (match.hasMatch()) { QString contents = protectEnc(match.captured(1)); marked.replace(match.capturedStart(1), match.capturedLength(1), contents); } // Look for the _ character in the member name followed by a number (or n): // this is intended to be rendered as a subscript. static const QRegularExpression re("<@param>([a-z]+)_([0-9]+|n)"); marked.replace(re, "\\1\\2"); // Replace some markup by HTML tags. Do both the opening and the closing tag // in one go (instead of <@param> and separately, for instance). marked.replace("@param>", "i>"); marked.replace("@extra>", "code>"); if (summary) { marked.remove("<@name>"); marked.remove(""); marked.remove("<@type>"); marked.remove(""); } 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; out() << "
        \n"; for (const auto *node : cn->members()) { out() << "
      • "; generateFullName(node, nullptr); out() << "
      • \n"; } out() << "
      \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(relative); auto *collectionNode = static_cast(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() << "
      \n"; } else { if (twoColumn) out() << "
      \n" << "\n"; else out() << "\n"; i++; } if (alignNames) out() << "
      "; out() << "
        \n"; } int i = 0; for (const auto &member : nv) { if (member->access() == Access::Private) continue; if (alignNames) { out() << "
      "; } else { if (twoColumn && i == (nv.size() + 1) / 2) out() << "
        \n"; out() << "
      • "; } generateSynopsis(member, relative, marker, Section::Summary, alignNames); if (alignNames) out() << "
      \n"; else { out() << "
    \n"; if (twoColumn) out() << "\n\n"; } } } void HtmlGenerator::generateSectionList(const Section §ion, 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() << "
    \n"; } else { if (twoColumn) out() << "
    \n" << "\n"; else out() << "\n"; i++; } if (alignNames) out() << "
    "; out() << "
      \n"; } int i = 0; for (const auto &member : members) { if (member->access() == Access::Private) continue; if (alignNames) { out() << "
    "; } else { if (twoColumn && i == (members.size() + 1) / 2) out() << "
      \n"; out() << "
    • "; } generateSynopsis(member, relative, marker, section.style(), alignNames); if (member->isFunction()) { const auto *fn = static_cast(member); if (fn->isPrivateSignal()) { hasPrivateSignals = true; if (alignNames) out() << "
    [see note below]"; } else if (fn->isInvokable()) { isInvokable = true; if (alignNames) out() << "[see note below]"; } } if (alignNames) out() << "
    \n"; else { out() << "
\n"; if (twoColumn) out() << "\n\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() << "
    \n"; generateSectionInheritedList(section, relative); out() << "
\n"; } } void HtmlGenerator::generateSectionInheritedList(const Section §ion, const Node *relative) { const QList> &inheritedMembers = section.inheritedMembers(); for (const auto &member : inheritedMembers) { out() << "
  • "; out() << member.second << ' '; if (member.second == 1) { out() << section.singular(); } else { out() << section.plural(); } out() << " inherited from " << protectEnc(member.first->plainFullName(relative)) << "
  • \n"; } } void HtmlGenerator::generateSynopsis(const Node *node, const Node *relative, CodeMarker *marker, Section::Style style, bool alignNames) { QString marked = marker->markedUpSynopsis(node, relative, style); static const QRegularExpression templateTag("(<[^@>]*>)"); auto match = templateTag.match(marked); if (match.hasMatch()) { QString contents = protectEnc(match.captured(1)); marked.replace(match.capturedStart(1), match.capturedLength(1), contents); } static const QRegularExpression re("<@param>([a-z]+)_([1-9n])"); marked.replace(re, "\\1\\2"); marked.replace("<@param>", ""); marked.replace("", ""); if (style == Section::Summary) { marked.remove("<@name>"); // was "" marked.remove(""); // was "" } if (style == Section::AllMembers) { static const QRegularExpression extraRegExp("<@extra>.*", QRegularExpression::InvertedGreedinessOption); marked.remove(extraRegExp); } else { marked.replace("<@extra>", ""); marked.replace("", ""); } if (style != Section::Details) { marked.remove("<@type>"); marked.remove(""); } 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=\"([^\"]+)\">).*()" // replace all <@func> tags: "(<@func target=\"([^\"]*)\">)(.*)()" // replace all "(<@(type|headerfile)(?: +[^>]*)?>)(.*)()" 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(""); done = true; } i += 2; if (parseArg(src, linkTag, &i, srcSize, &arg, &par1)) { html += QLatin1String(""); const Node *n = CodeMarker::nodeForString(par1.toString()); QString link = linkForNode(n, relative); addLink(link, arg, &html); html += QLatin1String(""); } 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(""); 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(""); } 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>" -> ""; // "<@preprocessor>" -> ""; // "<@string>" -> ""; // "<@char>" -> ""; // "<@number>" -> ""; // "<@op>" -> ""; // "<@type>" -> ""; // "<@name>" -> ""; // "<@keyword>" -> ""; // "" -> "" src = html; html = QString(); html.reserve(src.size()); static const QLatin1String spanTags[] = { QLatin1String("comment>"), QLatin1String(""), QLatin1String("preprocessor>"), QLatin1String(""), QLatin1String("string>"), QLatin1String(""), QLatin1String("char>"), QLatin1String(""), QLatin1String("number>"), QLatin1String(""), QLatin1String("op>"), QLatin1String(""), QLatin1String("type>"), QLatin1String(""), QLatin1String("name>"), QLatin1String(""), QLatin1String("keyword>"), QLatin1String("") }; 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(""); 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("&"); } else if (ch == QLatin1Char('<')) { APPEND("<"); } else if (ch == QLatin1Char('>')) { APPEND(">"); } else if (ch == QChar(8211)) { APPEND("–"); } else if (ch == QChar(8212)) { APPEND("—"); } else if (ch == QLatin1Char('"')) { APPEND("""); } else if ((ch == QLatin1Char('*') && i + 1 < n && string.at(i) == QLatin1Char('/')) || (ch == QLatin1Char('.') && i > 2 && string.at(i - 2) == QLatin1Char('.'))) { // we escape '*/' and the last dot in 'e.g.' and 'i.e.' for the Javadoc generator APPEND("&#x"); html += QString::number(ch.unicode(), 16); html += QLatin1Char(';'); } 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() << "isDeprecated()) out() << "\" class=\"obsolete"; out() << "\">"; } out() << protectEnc(apparentNode->fullName(relative)); if (link) out() << ""; } 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(node); const QList &collective = scn->collective(); if (collective.size() > 1) out() << "
    \n"; for (const auto *sharedNode : collective) { nodeRef = refForNode(sharedNode); out() << R"(

    "; generateSynopsis(sharedNode, relative, marker, Section::Details); out() << "

    "; } if (collective.size() > 1) out() << "
    "; out() << '\n'; } else { nodeRef = refForNode(node); if (node->isEnumType() && (etn = static_cast(node))->flagsType()) { out() << R"(

    "; generateSynopsis(etn, relative, marker, Section::Details); out() << "
    "; generateSynopsis(etn->flagsType(), relative, marker, Section::Details); out() << "

    \n"; } else { out() << R"(

    "; generateSynopsis(node, relative, marker, Section::Details); out() << "

    " << '\n'; } } generateStatus(node, marker); generateBody(node, marker); generateOverloadedSignal(node, marker); generateThreadSafeness(node, marker); generateSince(node, marker); generateNoexceptNote(node, marker); if (node->isProperty()) { const auto property = static_cast(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() << "

    Access functions:

    \n"; generateSectionList(section, node, marker); } Section notifiers("", "", "", "", Section::Accessors); notifiers.appendMembers(property->notifiers().toVector()); if (!notifiers.members().isEmpty()) { out() << "

    Notifier signal:

    \n"; generateSectionList(notifiers, node, marker); } } } else if (node->isEnumType()) { const auto *enumTypeNode = static_cast(node); if (enumTypeNode->flagsType()) { out() << "

    The " << protectEnc(enumTypeNode->flagsType()->name()) << " type is a typedef for " << "QFlags<" << protectEnc(enumTypeNode->name()) << ">. It stores an OR combination of " << protectEnc(enumTypeNode->name()) << " values.

    \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() << ""; } 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; if (node == nullptr || (relative != nullptr && node->status() == relative->status())) out() << ""; else if (node->isDeprecated()) out() << ""; else out() << ""; } void HtmlGenerator::endLink() { if (!m_inLink) return; m_inLink = false; m_linkNode = nullptr; if (!m_link.isEmpty()) out() << ""; } /*! 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() << "
      \n"; for (const auto &member : members) { out() << "
    • "; generateQmlItem(member, relative, marker, true); if (member->isPropertyGroup()) { const auto *scn = static_cast(member); if (scn->count() > 0) { out() << "
        \n"; const QList &sharedNodes = scn->collective(); for (const auto &node : sharedNodes) { if (node->isQmlProperty()) { out() << "
      • "; generateQmlItem(node, relative, marker, true); out() << "
      • \n"; } } out() << "
      \n"; } } out() << "
    • \n"; } out() << "
    \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("
    \n" "
    \n"); QString qmlItemStart("\n" "\n"); QString qmlItemFooter("

    \n"); QString qmlItemEnd("

    \n"); std::function generateQmlProperty = [&](QmlPropertyNode *n) { out() << qmlItemStart.arg(refForNode(n), "tblQmlPropNode"); if (!n->isReadOnlySet() && n->declarativeCppNode()) n->markReadOnly(!n->isWritable()); QStringList extra; if (n->isDefault()) extra << "default"; else if (n->isReadOnly()) extra << "read-only"; else if (n->isRequired()) extra << "required"; else if (!n->defaultValue().isEmpty()) { extra << "default: " + n->defaultValue(); } if (!n->since().isEmpty()) { if (!extra.isEmpty()) extra.last().append(','); extra << "since " + n->since(); } if (!extra.isEmpty()) out() << QString("[%1] ") .arg(extra.join(QLatin1Char(' '))); generateQmlItem(n, relative, marker, false); out() << qmlItemEnd; }; std::function generateQmlMethod = [&](Node *n) { out() << qmlItemStart.arg(refForNode(n), "tblQmlFuncNode"); generateSynopsis(n, relative, marker, Section::Details, false); out() << qmlItemEnd; }; out() << "
    "; if (node->isPropertyGroup()) { const auto *scn = static_cast(node); out() << qmlItemHeader; if (!scn->name().isEmpty()) { const QString nodeRef = refForNode(scn); out() << R"("; out() << "

    "; out() << "" << scn->name() << " group"; out() << "

    \n"; } const QList sharedNodes = scn->collective(); for (const auto &sharedNode : sharedNodes) { if (sharedNode->isQmlProperty()) generateQmlProperty(static_cast(sharedNode)); } out() << qmlItemFooter; } else if (node->isQmlProperty()) { out() << qmlItemHeader; generateQmlProperty(static_cast(node)); out() << qmlItemFooter; } else if (node->isSharedCommentNode()) { const auto *scn = reinterpret_cast(node); const QList &sharedNodes = scn->collective(); if (sharedNodes.size() > 1) out() << "
    \n"; out() << qmlItemHeader; for (const auto &sharedNode : sharedNodes) { // Generate the node only if it is relevant for Qt Quick. if (sharedNode->isFunction(Node::QML)) generateQmlMethod(sharedNode); else if (sharedNode->isQmlProperty()) generateQmlProperty(static_cast(sharedNode)); } out() << qmlItemFooter; if (sharedNodes.size() > 1) out() << "
    "; // fngroup } else { // assume the node is a method/signal handler out() << qmlItemHeader; generateQmlMethod(node); out() << qmlItemFooter; } out() << "
    "; generateStatus(node, marker); generateBody(node, marker); generateThreadSafeness(node, marker); generateSince(node, marker); generateAlsoList(node, marker); out() << "
    "; generateExtractionMark(node, EndMark); } void HtmlGenerator::generateExtractionMark(const Node *node, ExtractionMarkType markType) { if (markType != EndMark) { out() << "\n"; } else { out() << "\n"; } } QT_END_NAMESPACE