// 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 ";
rewritePropertyBrief(atom, relative);
break;
case Atom::BriefRight:
if (hasBrief(relative))
out() << " ";
in_para = true;
break;
case Atom::CaptionRight:
endLink();
if (in_para) {
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() << ""
<< 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() << "" << protectEnc(atom->string()) << "
\n";
else
out() << "...
\n";
break;
case Atom::DetailsRight:
out() << "
Class " : "
QML Type "); out() << ""; QStringList pieces = parent->fullName().split("::"); out() << protectEnc(pieces.last()); out() << "" << ":
\n"; generateSection(nv, relative, marker); out() << "enum value | " << "" << it.key() << " |
"; 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() << ""; out() << formattingLeftMap()[ATOM_FORMATTING_BOLD]; out() << admonType << ": "; out() << formattingRightMap()[ATOM_FORMATTING_BOLD]; } break; case Atom::ImportantRight: case Atom::NoteRight: case Atom::WarningRight: out() << "
\n" << "Constant | "; // If not in \enum topic, skip the value column if (relative->isEnumType()) out() << "Value | "; out() << "Description |
---|---|---|
Constant | Value | |
" << t << " ";
if (relative->isEnumType()) {
out() << " | ";
const auto *enume = static_cast" << 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() << " | ";
if (matchAhead(atom, Atom::ListItemRight))
out() << " ";
}
} else {
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() << "
\)" << 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"; 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() << ""; generateText(brief, cn, marker); out() << "
\n"; const QList)";
} else {
out() << " | \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 ||||||||||||||||||
" << *it << ":" " | "; if (*it == headerText) out() << requisites.value(*it).toString(); else generateText(requisites.value(*it), aggregate, marker); out() << " |
" << requisite << " | "; if (requisite == importText) out() << requisites.value(requisite).toString(); else generateText(requisites.value(requisite), qcn, marker); 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, QListThis 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::functionThe 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() << "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() << ""; generateFullName(node, relative); out() << " | ";
if (!node->isTextPageNode()) {
Text brief = node->doc().trimmedBriefText(node->name());
if (!brief.isEmpty()) {
out() << ""; generateText(brief, node, marker); out() << " | ";
} else if (!node->reconstitutedBrief().isEmpty()) {
out() << ""; out() << node->reconstitutedBrief(); out() << " | ";
}
} else {
out() << ""; if (!node->reconstitutedBrief().isEmpty()) { out() << node->reconstitutedBrief(); } else out() << protectEnc(node->doc().briefText().toString()); out() << " | ";
}
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"; 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() << "";
out() << "
";
} else {
if (twoColumn && i == (nv.size() + 1) / 2)
out() << " |
|
";
out() << "
";
} else {
if (twoColumn && i == (members.size() + 1) / 2)
out() << " |
[see note below]";
} else if (fn->isInvokable()) {
isInvokable = true;
if (alignNames)
out() << " | [see note below]";
}
}
if (alignNames)
out() << " | |
");
marked.replace("@extra>", "
");
}
if (style != Section::Details) {
marked.remove("<@type>");
marked.remove("@type>");
}
out() << highlightedCode(marked, relative, alignNames);
}
QString HtmlGenerator::highlightedCode(const QString &markedCode, const Node *relative,
bool alignNames, Node::Genus genus)
{
QString src = markedCode;
QString html;
html.reserve(src.size());
QStringView arg;
QStringView par1;
const QChar charLangle = '<';
const QChar charAt = '@';
static const QString typeTag("type");
static const QString headerTag("headerfile");
static const QString funcTag("func");
static const QString linkTag("link");
// replace all <@link> tags: "(<@link node=\"([^\"]+)\">).*(@link>)"
// replace all <@func> tags: "(<@func target=\"([^\"]*)\">)(.*)(@func>)"
// replace all "(<@(type|headerfile)(?: +[^>]*)?>)(.*)(@\\2>)" tags
bool done = false;
for (int i = 0, srcSize = src.size(); i < srcSize;) {
if (src.at(i) == charLangle && src.at(i + 1) == charAt) {
if (alignNames && !done) {
html += QLatin1String("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_castThe " << 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"); QString qmlItemEnd(" |
"; out() << "" << scn->name() << " group"; out() << "