| /**************************************************************************** |
| ** |
| ** Copyright (C) 2019 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the tools applications of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| /* |
| generator.cpp |
| */ |
| #include "generator.h" |
| |
| #include "codemarker.h" |
| #include "config.h" |
| #include "doc.h" |
| #include "editdistance.h" |
| #include "loggingcategory.h" |
| #include "node.h" |
| #include "openedlist.h" |
| #include "qdocdatabase.h" |
| #include "quoter.h" |
| #include "separator.h" |
| #include "tokenizer.h" |
| |
| #include <QtCore/qdebug.h> |
| #include <QtCore/qdir.h> |
| |
| #ifndef QT_BOOTSTRAPPED |
| # include "QtCore/qurl.h" |
| #endif |
| |
| QT_BEGIN_NAMESPACE |
| |
| Generator *Generator::currentGenerator_; |
| QStringList Generator::exampleDirs; |
| QStringList Generator::exampleImgExts; |
| QMap<QString, QMap<QString, QString>> Generator::fmtLeftMaps; |
| QMap<QString, QMap<QString, QString>> Generator::fmtRightMaps; |
| QVector<Generator *> Generator::generators; |
| QStringList Generator::imageDirs; |
| QStringList Generator::imageFiles; |
| QMap<QString, QStringList> Generator::imgFileExts; |
| QString Generator::outDir_; |
| QString Generator::outSubdir_; |
| QStringList Generator::outFileNames_; |
| QSet<QString> Generator::outputFormats; |
| QHash<QString, QString> Generator::outputPrefixes; |
| QHash<QString, QString> Generator::outputSuffixes; |
| QString Generator::project_; |
| QStringList Generator::scriptDirs; |
| QStringList Generator::scriptFiles; |
| QStringList Generator::styleDirs; |
| QStringList Generator::styleFiles; |
| bool Generator::noLinkErrors_ = false; |
| bool Generator::autolinkErrors_ = false; |
| bool Generator::redirectDocumentationToDevNull_ = false; |
| bool Generator::qdocSingleExec_ = false; |
| bool Generator::useOutputSubdirs_ = true; |
| QmlTypeNode *Generator::qmlTypeContext_ = nullptr; |
| |
| static QRegExp tag("</?@[^>]*>"); |
| static QLatin1String amp("&"); |
| static QLatin1String gt(">"); |
| static QLatin1String lt("<"); |
| static QLatin1String quot("""); |
| |
| /*! |
| Constructs the generator base class. Prepends the newly |
| constructed generator to the list of output generators. |
| Sets a pointer to the QDoc database singleton, which is |
| available to the generator subclasses. |
| */ |
| Generator::Generator() |
| : inLink_(false), |
| inContents_(false), |
| inSectionHeading_(false), |
| inTableHeader_(false), |
| threeColumnEnumValueTable_(true), |
| showInternal_(false), |
| singleExec_(false), |
| numTableRows_(0) |
| { |
| qdb_ = QDocDatabase::qdocDB(); |
| generators.prepend(this); |
| } |
| |
| /*! |
| Destroys the generator after removing it from the list of |
| output generators. |
| */ |
| Generator::~Generator() |
| { |
| generators.removeAll(this); |
| } |
| |
| void Generator::appendFullName(Text &text, const Node *apparentNode, const Node *relative, |
| const Node *actualNode) |
| { |
| if (actualNode == nullptr) |
| actualNode = apparentNode; |
| text << Atom(Atom::LinkNode, CodeMarker::stringForNode(actualNode)) |
| << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) |
| << Atom(Atom::String, apparentNode->plainFullName(relative)) |
| << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); |
| } |
| |
| void Generator::appendFullName(Text &text, const Node *apparentNode, const QString &fullName, |
| const Node *actualNode) |
| { |
| if (actualNode == nullptr) |
| actualNode = apparentNode; |
| text << Atom(Atom::LinkNode, CodeMarker::stringForNode(actualNode)) |
| << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << Atom(Atom::String, fullName) |
| << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); |
| } |
| |
| void Generator::appendFullNames(Text &text, const NodeList &nodes, const Node *relative) |
| { |
| int index = 0; |
| for (const auto &node : nodes) { |
| appendFullName(text, node, relative); |
| text << comma(index++, nodes.count()); |
| } |
| } |
| |
| /*! |
| Append the signature for the function named in \a node to |
| \a text, so that is is a link to the documentation for that |
| function. |
| */ |
| void Generator::appendSignature(Text &text, const Node *node) |
| { |
| text << Atom(Atom::LinkNode, CodeMarker::stringForNode(node)) |
| << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) |
| << Atom(Atom::String, node->signature(false, true)) |
| << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); |
| } |
| |
| /*! |
| Generate a bullet list of function signatures. The function |
| nodes are in \a nodes. It uses the \a relative node and the |
| \a marker for the generation. |
| */ |
| void Generator::signatureList(const NodeList &nodes, const Node *relative, CodeMarker *marker) |
| { |
| Text text; |
| int count = 0; |
| text << Atom(Atom::ListLeft, QString("bullet")); |
| for (const auto &node : nodes) { |
| text << Atom(Atom::ListItemNumber, QString::number(++count)); |
| text << Atom(Atom::ListItemLeft, QString("bullet")); |
| appendSignature(text, node); |
| text << Atom(Atom::ListItemRight, QString("bullet")); |
| } |
| text << Atom(Atom::ListRight, QString("bullet")); |
| generateText(text, relative, marker); |
| } |
| |
| int Generator::appendSortedNames(Text &text, const ClassNode *cn, const QVector<RelatedClass> &rc) |
| { |
| QMap<QString, Text> classMap; |
| for (const auto &relatedClass : rc) { |
| ClassNode *rcn = relatedClass.node_; |
| if (rcn && rcn->isInAPI()) { |
| Text className; |
| appendFullName(className, rcn, cn); |
| classMap[className.toString().toLower()] = className; |
| } |
| } |
| |
| int index = 0; |
| const QStringList classNames = classMap.keys(); |
| for (const auto &className : classNames) { |
| text << classMap[className]; |
| text << comma(index++, classNames.count()); |
| } |
| return index; |
| } |
| |
| int Generator::appendSortedQmlNames(Text &text, const Node *base, const NodeList &subs) |
| { |
| QMap<QString, Text> classMap; |
| |
| for (const auto sub : subs) { |
| Text text; |
| if (!base->isQtQuickNode() || !sub->isQtQuickNode() |
| || (base->logicalModuleName() == sub->logicalModuleName())) { |
| appendFullName(text, sub, base); |
| classMap[text.toString().toLower()] = text; |
| } |
| } |
| |
| int index = 0; |
| const QStringList names = classMap.keys(); |
| for (const auto &name : names) { |
| text << classMap[name]; |
| text << comma(index++, names.count()); |
| } |
| return index; |
| } |
| |
| /*! |
| For debugging qdoc. |
| */ |
| void Generator::writeOutFileNames() |
| { |
| QFile files("outputlist.txt"); |
| if (!files.open(QFile::WriteOnly)) |
| return; |
| QTextStream filesout(&files); |
| const auto names = outFileNames_; |
| for (const auto &file : names) { |
| filesout << file << "\n"; |
| } |
| } |
| |
| /*! |
| Creates the file named \a fileName in the output directory |
| and returns a QFile pointing to this file. In particular, |
| this method deals with errors when opening the file: |
| the returned QFile is always valid and can be written to. |
| |
| \sa beginFilePage() |
| */ |
| QFile *Generator::openSubPageFile(const Node *node, const QString &fileName) |
| { |
| QString path = outputDir() + QLatin1Char('/'); |
| if (Generator::useOutputSubdirs() && !node->outputSubdirectory().isEmpty() |
| && !outputDir().endsWith(node->outputSubdirectory())) { |
| path += node->outputSubdirectory() + QLatin1Char('/'); |
| } |
| path += fileName; |
| |
| auto outPath = redirectDocumentationToDevNull_ ? QStringLiteral("/dev/null") : path; |
| auto outFile = new QFile(outPath); |
| if (!redirectDocumentationToDevNull_ && outFile->exists()) { |
| node->location().error( |
| tr("Output file already exists; overwriting %1").arg(outFile->fileName())); |
| } |
| if (!outFile->open(QFile::WriteOnly)) { |
| node->location().fatal(tr("Cannot open output file '%1'").arg(outFile->fileName())); |
| } |
| qCDebug(lcQdoc, "Writing: %s", qPrintable(path)); |
| outFileNames_ << fileName; |
| return outFile; |
| } |
| |
| /*! |
| Creates the file named \a fileName in the output directory. |
| Attaches a QTextStream to the created file, which is written |
| to all over the place using out(). This function does not |
| store the \a fileName in the \a node as the output file name. |
| |
| \sa beginSubPage() |
| */ |
| void Generator::beginFilePage(const Node *node, const QString &fileName) |
| { |
| QFile *outFile = openSubPageFile(node, fileName); |
| QTextStream *out = new QTextStream(outFile); |
| #ifndef QT_NO_TEXTCODEC |
| if (outputCodec) |
| out->setCodec(outputCodec); |
| #endif |
| outStreamStack.push(out); |
| } |
| |
| /*! |
| Creates the file named \a fileName in the output directory. |
| Attaches a QTextStream to the created file, which is written |
| to all over the place using out(). This function calls another |
| function, \c beginFilePage(), which is really just most of what |
| this function used to contain. We needed a different version |
| that doesn't store the \a fileName in the \a node as the output |
| file name. |
| |
| \sa beginFilePage() |
| */ |
| void Generator::beginSubPage(const Node *node, const QString &fileName) |
| { |
| beginFilePage(node, fileName); |
| const_cast<Node *>(node)->setOutputFileName(fileName); |
| } |
| |
| /*! |
| Flush the text stream associated with the subpage, and |
| then pop it off the text stream stack and delete it. |
| This terminates output of the subpage. |
| */ |
| void Generator::endSubPage() |
| { |
| outStreamStack.top()->flush(); |
| delete outStreamStack.top()->device(); |
| delete outStreamStack.pop(); |
| } |
| |
| /* |
| the code below is effectively equivalent to: |
| input.replace(QRegExp("[^A-Za-z0-9]+"), " "); |
| input = input.trimmed(); |
| input.replace(QLatin1Char(' '), QLatin1Char('-')); |
| input = input.toLower(); |
| as this function accounted for ~8% of total running time |
| we optimize a bit... |
| */ |
| static void transmogrify(QString &input, QString &output) |
| { |
| // +5 prevents realloc in fileName() below |
| output.reserve(input.size() + 5); |
| bool begun = false; |
| for (int i = 0; i != input.size(); ++i) { |
| QChar c = input.at(i); |
| uint u = c.unicode(); |
| if (u >= 'A' && u <= 'Z') |
| u += 'a' - 'A'; |
| if ((u >= 'a' && u <= 'z') || (u >= '0' && u <= '9')) { |
| output += QLatin1Char(u); |
| begun = true; |
| } else if (begun) { |
| output += QLatin1Char('-'); |
| begun = false; |
| } |
| } |
| while (output.endsWith(QLatin1Char('-'))) |
| output.chop(1); |
| } |
| |
| QString Generator::fileBase(const Node *node) const |
| { |
| if (!node->isPageNode() && !node->isCollectionNode()) |
| node = node->parent(); |
| |
| if (node->hasFileNameBase()) |
| return node->fileNameBase(); |
| |
| QString base; |
| if (node->isCollectionNode()) { |
| base = node->name() + outputSuffix(node); |
| if (base.endsWith(".html")) |
| base.truncate(base.length() - 5); |
| |
| if (node->isQmlModule()) |
| base.append("-qmlmodule"); |
| else if (node->isJsModule()) |
| base.append("-jsmodule"); |
| else if (node->isModule()) |
| base.append("-module"); |
| // Why not add "-group" for group pages? |
| } else if (node->isTextPageNode()) { |
| base = node->name(); |
| if (base.endsWith(".html")) |
| base.truncate(base.length() - 5); |
| |
| if (node->isExample()) { |
| QString modPrefix(node->physicalModuleName()); |
| if (modPrefix.isEmpty()) { |
| modPrefix = project_; |
| } |
| base.prepend(modPrefix.toLower() + QLatin1Char('-')); |
| } |
| if (node->isExample()) { |
| base.append(QLatin1String("-example")); |
| } |
| } else if (node->isQmlType() || node->isQmlBasicType() || node->isJsType() |
| || node->isJsBasicType()) { |
| base = node->name(); |
| /* |
| To avoid file name conflicts in the html directory, |
| we prepend a prefix (by default, "qml-") and an optional suffix |
| to the file name. The suffix, if one exists, is appended to the |
| module name. |
| */ |
| if (!node->logicalModuleName().isEmpty() |
| && (!node->logicalModule()->isInternal() || showInternal_)) |
| base.prepend(node->logicalModuleName() + outputSuffix(node) + QLatin1Char('-')); |
| |
| base.prepend(outputPrefix(node)); |
| } else if (node->isProxyNode()) { |
| base = node->name(); |
| base.append("-proxy"); |
| } else { |
| const Node *p = node; |
| forever { |
| const Node *pp = p->parent(); |
| base.prepend(p->name()); |
| if (pp == nullptr || pp->name().isEmpty() || pp->isTextPageNode()) |
| break; |
| base.prepend(QLatin1Char('-')); |
| p = pp; |
| } |
| if (node->isNamespace() && !node->name().isEmpty()) { |
| const NamespaceNode *ns = static_cast<const NamespaceNode *>(node); |
| if (!ns->isDocumentedHere()) { |
| base.append(QLatin1String("-sub-")); |
| base.append(ns->tree()->camelCaseModuleName()); |
| } |
| } |
| } |
| |
| QString res; |
| transmogrify(base, res); |
| Node *n = const_cast<Node *>(node); |
| n->setFileNameBase(res); |
| return res; |
| } |
| |
| /*! |
| Constructs an href link from an example file name, which |
| is a path to the example file. If \a fileExtension is |
| empty (default value), retrieve the file extension from |
| the generator. |
| */ |
| QString Generator::linkForExampleFile(const QString &path, const Node *parent, |
| const QString &fileExt) |
| { |
| QString link = path; |
| QString modPrefix(parent->physicalModuleName()); |
| if (modPrefix.isEmpty()) |
| modPrefix = project_; |
| link.prepend(modPrefix.toLower() + QLatin1Char('-')); |
| |
| QString res; |
| transmogrify(link, res); |
| res.append(QLatin1Char('.')); |
| res.append(fileExt); |
| if (fileExt.isEmpty()) |
| res.append(fileExtension()); |
| return res; |
| } |
| |
| /*! |
| Helper function to construct a title for a file or image page |
| included in an example. |
| */ |
| QString Generator::exampleFileTitle(const ExampleNode *relative, const QString &fileName) |
| { |
| QString suffix; |
| if (relative->files().contains(fileName)) |
| suffix = QLatin1String(" Example File"); |
| else if (relative->images().contains(fileName)) |
| suffix = QLatin1String(" Image File"); |
| else |
| return suffix; |
| |
| return fileName.mid(fileName.lastIndexOf(QLatin1Char('/')) + 1) + suffix; |
| } |
| |
| /*! |
| If the \a node has a URL, return the URL as the file name. |
| Otherwise, construct the file name from the fileBase() and |
| either the provided \a extension or fileExtension(), and |
| return the constructed name. |
| */ |
| QString Generator::fileName(const Node *node, const QString &extension) const |
| { |
| if (!node->url().isEmpty()) |
| return node->url(); |
| |
| QString name = fileBase(node) + QLatin1Char('.'); |
| return extension.isNull() ? name + fileExtension() : name + extension; |
| } |
| |
| QString Generator::cleanRef(const QString &ref) |
| { |
| QString clean; |
| |
| if (ref.isEmpty()) |
| return clean; |
| |
| clean.reserve(ref.size() + 20); |
| const QChar c = ref[0]; |
| const uint u = c.unicode(); |
| |
| if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9')) { |
| clean += c; |
| } else if (u == '~') { |
| clean += "dtor."; |
| } else if (u == '_') { |
| clean += "underscore."; |
| } else { |
| clean += QLatin1Char('A'); |
| } |
| |
| for (int i = 1; i < ref.length(); i++) { |
| const QChar c = ref[i]; |
| const uint u = c.unicode(); |
| if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9') || u == '-' |
| || u == '_' || u == ':' || u == '.') { |
| clean += c; |
| } else if (c.isSpace()) { |
| clean += QLatin1Char('-'); |
| } else if (u == '!') { |
| clean += "-not"; |
| } else if (u == '&') { |
| clean += "-and"; |
| } else if (u == '<') { |
| clean += "-lt"; |
| } else if (u == '=') { |
| clean += "-eq"; |
| } else if (u == '>') { |
| clean += "-gt"; |
| } else if (u == '#') { |
| clean += QLatin1Char('#'); |
| } else { |
| clean += QLatin1Char('-'); |
| clean += QString::number(static_cast<int>(u), 16); |
| } |
| } |
| return clean; |
| } |
| |
| QMap<QString, QString> &Generator::formattingLeftMap() |
| { |
| return fmtLeftMaps[format()]; |
| } |
| |
| QMap<QString, QString> &Generator::formattingRightMap() |
| { |
| return fmtRightMaps[format()]; |
| } |
| |
| /*! |
| Returns the full document location. |
| */ |
| QString Generator::fullDocumentLocation(const Node *node, bool useSubdir) |
| { |
| if (node == nullptr) |
| return QString(); |
| if (!node->url().isEmpty()) |
| return node->url(); |
| |
| QString parentName; |
| QString anchorRef; |
| QString fdl; |
| |
| /* |
| If the useSubdir parameter is set, then the output is |
| being sent to subdirectories of the output directory. |
| Prepend the subdirectory name + '/' to the result. |
| */ |
| if (useSubdir) { |
| fdl = node->outputSubdirectory(); |
| if (!fdl.isEmpty()) |
| fdl.append(QLatin1Char('/')); |
| } |
| if (node->isNamespace()) { |
| /* |
| The root namespace has no name - check for this before creating |
| an attribute containing the location of any documentation. |
| */ |
| if (!fileBase(node).isEmpty()) |
| parentName = fileBase(node) + QLatin1Char('.') + currentGenerator()->fileExtension(); |
| else |
| return QString(); |
| } else if (node->isQmlType() || node->isQmlBasicType() || node->isJsType() |
| || node->isJsBasicType()) { |
| QString fb = fileBase(node); |
| if (fb.startsWith(outputPrefix(node))) |
| return fb + QLatin1Char('.') + currentGenerator()->fileExtension(); |
| else { |
| QString mq; |
| if (!node->logicalModuleName().isEmpty()) { |
| mq = node->logicalModuleName().replace(QChar('.'), QChar('-')); |
| mq = mq.toLower() + QLatin1Char('-'); |
| } |
| return fdl + outputPrefix(node) + mq + fileBase(node) + QLatin1Char('.') |
| + currentGenerator()->fileExtension(); |
| } |
| } else if (node->isTextPageNode() || node->isCollectionNode()) { |
| parentName = fileBase(node) + QLatin1Char('.') + currentGenerator()->fileExtension(); |
| } else if (fileBase(node).isEmpty()) |
| return QString(); |
| |
| Node *parentNode = nullptr; |
| |
| if ((parentNode = node->parent())) { |
| // use the parent's name unless the parent is the root namespace |
| if (!node->parent()->isNamespace() || !node->parent()->name().isEmpty()) |
| parentName = fullDocumentLocation(node->parent()); |
| } |
| |
| switch (node->nodeType()) { |
| case Node::Class: |
| case Node::Struct: |
| case Node::Union: |
| case Node::Namespace: |
| case Node::Proxy: |
| parentName = fileBase(node) + QLatin1Char('.') + currentGenerator()->fileExtension(); |
| break; |
| case Node::Function: { |
| const FunctionNode *fn = static_cast<const FunctionNode *>(node); |
| switch (fn->metaness()) { |
| case FunctionNode::JsSignal: |
| case FunctionNode::QmlSignal: |
| anchorRef = QLatin1Char('#') + node->name() + "-signal"; |
| break; |
| case FunctionNode::JsSignalHandler: |
| case FunctionNode::QmlSignalHandler: |
| anchorRef = QLatin1Char('#') + node->name() + "-signal-handler"; |
| break; |
| case FunctionNode::JsMethod: |
| case FunctionNode::QmlMethod: |
| anchorRef = QLatin1Char('#') + node->name() + "-method"; |
| break; |
| default: |
| if (fn->isDtor()) |
| anchorRef = "#dtor." + fn->name().mid(1); |
| else if (fn->hasOneAssociatedProperty() && fn->doc().isEmpty()) |
| return fullDocumentLocation(fn->firstAssociatedProperty()); |
| else if (fn->overloadNumber() > 0) |
| anchorRef = QLatin1Char('#') + cleanRef(fn->name()) + QLatin1Char('-') |
| + QString::number(fn->overloadNumber()); |
| else |
| anchorRef = QLatin1Char('#') + cleanRef(fn->name()); |
| break; |
| } |
| break; |
| } |
| /* |
| Use node->name() instead of fileBase(node) as |
| the latter returns the name in lower-case. For |
| HTML anchors, we need to preserve the case. |
| */ |
| case Node::Enum: |
| anchorRef = QLatin1Char('#') + node->name() + "-enum"; |
| break; |
| case Node::TypeAlias: |
| anchorRef = QLatin1Char('#') + node->name() + "-alias"; |
| break; |
| case Node::Typedef: { |
| const TypedefNode *tdef = static_cast<const TypedefNode *>(node); |
| if (tdef->associatedEnum()) { |
| return fullDocumentLocation(tdef->associatedEnum()); |
| } |
| anchorRef = QLatin1Char('#') + node->name() + "-typedef"; |
| break; |
| } |
| case Node::Property: |
| anchorRef = QLatin1Char('#') + node->name() + "-prop"; |
| break; |
| case Node::JsProperty: |
| case Node::QmlProperty: |
| if (node->isAttached()) |
| anchorRef = QLatin1Char('#') + node->name() + "-attached-prop"; |
| else |
| anchorRef = QLatin1Char('#') + node->name() + "-prop"; |
| break; |
| case Node::Variable: |
| anchorRef = QLatin1Char('#') + node->name() + "-var"; |
| break; |
| case Node::JsType: |
| case Node::QmlType: |
| case Node::Page: |
| case Node::Group: |
| case Node::HeaderFile: |
| case Node::Module: |
| case Node::JsModule: |
| case Node::QmlModule: { |
| parentName = fileBase(node); |
| parentName.replace(QLatin1Char('/'), QLatin1Char('-')) |
| .replace(QLatin1Char('.'), QLatin1Char('-')); |
| parentName += QLatin1Char('.') + currentGenerator()->fileExtension(); |
| } break; |
| default: |
| break; |
| } |
| |
| if (!node->isClassNode() && !node->isNamespace()) { |
| if (node->isObsolete()) |
| parentName.replace(QLatin1Char('.') + currentGenerator()->fileExtension(), |
| "-obsolete." + currentGenerator()->fileExtension()); |
| } |
| |
| return fdl + parentName.toLower() + anchorRef; |
| } |
| |
| void Generator::generateAlsoList(const Node *node, CodeMarker *marker) |
| { |
| QVector<Text> alsoList = node->doc().alsoList(); |
| supplementAlsoList(node, alsoList); |
| |
| if (!alsoList.isEmpty()) { |
| Text text; |
| text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) << "See also " |
| << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD); |
| |
| for (int i = 0; i < alsoList.size(); ++i) |
| text << alsoList.at(i) << separator(i, alsoList.size()); |
| |
| text << Atom::ParaRight; |
| generateText(text, node, marker); |
| } |
| } |
| |
| const Atom *Generator::generateAtomList(const Atom *atom, const Node *relative, CodeMarker *marker, |
| bool generate, int &numAtoms) |
| { |
| while (atom != nullptr) { |
| if (atom->type() == Atom::FormatIf) { |
| int numAtoms0 = numAtoms; |
| bool rightFormat = canHandleFormat(atom->string()); |
| atom = generateAtomList(atom->next(), relative, marker, generate && rightFormat, |
| numAtoms); |
| if (atom == nullptr) |
| return nullptr; |
| |
| if (atom->type() == Atom::FormatElse) { |
| ++numAtoms; |
| atom = generateAtomList(atom->next(), relative, marker, generate && !rightFormat, |
| numAtoms); |
| if (atom == nullptr) |
| return nullptr; |
| } |
| |
| if (atom->type() == Atom::FormatEndif) { |
| if (generate && numAtoms0 == numAtoms) { |
| relative->location().warning( |
| tr("Output format %1 not handled %2").arg(format()).arg(outFileName())); |
| Atom unhandledFormatAtom(Atom::UnhandledFormat, format()); |
| generateAtomList(&unhandledFormatAtom, relative, marker, generate, numAtoms); |
| } |
| atom = atom->next(); |
| } |
| } else if (atom->type() == Atom::FormatElse || atom->type() == Atom::FormatEndif) { |
| return atom; |
| } else { |
| int n = 1; |
| if (generate) { |
| n += generateAtom(atom, relative, marker); |
| numAtoms += n; |
| } |
| while (n-- > 0) |
| atom = atom->next(); |
| } |
| } |
| return nullptr; |
| } |
| |
| /*! |
| Generate the body of the documentation from the qdoc comment |
| found with the entity represented by the \a node. |
| */ |
| void Generator::generateBody(const Node *node, CodeMarker *marker) |
| { |
| const FunctionNode *fn = node->isFunction() ? static_cast<const FunctionNode *>(node) : nullptr; |
| if (!node->hasDoc() && !node->hasSharedDoc()) { |
| /* |
| Test for special function, like a destructor or copy constructor, |
| that has no documentation. |
| */ |
| if (fn) { |
| if (fn->isDtor()) { |
| Text text; |
| text << "Destroys the instance of "; |
| text << fn->parent()->name() << "."; |
| if (fn->isVirtual()) |
| text << " The destructor is virtual."; |
| out() << "<p>"; |
| generateText(text, node, marker); |
| out() << "</p>"; |
| } else if (fn->isCtor()) { |
| Text text; |
| text << "Default constructs an instance of "; |
| text << fn->parent()->name() << "."; |
| out() << "<p>"; |
| generateText(text, node, marker); |
| out() << "</p>"; |
| } else if (fn->isCCtor()) { |
| Text text; |
| text << "Copy constructor."; |
| out() << "<p>"; |
| generateText(text, node, marker); |
| out() << "</p>"; |
| } else if (fn->isMCtor()) { |
| Text text; |
| text << "Move-copy constructor."; |
| out() << "<p>"; |
| generateText(text, node, marker); |
| out() << "</p>"; |
| } else if (fn->isCAssign()) { |
| Text text; |
| text << "Copy-assignment operator."; |
| out() << "<p>"; |
| generateText(text, node, marker); |
| out() << "</p>"; |
| } else if (fn->isMAssign()) { |
| Text text; |
| text << "Move-assignment operator."; |
| out() << "<p>"; |
| generateText(text, node, marker); |
| out() << "</p>"; |
| } else if (!node->isWrapper() && !node->isMarkedReimp()) { |
| if (!fn->isIgnored()) // undocumented functions added by Q_OBJECT |
| node->location().warning( |
| tr("No documentation for '%1'").arg(node->plainSignature())); |
| } |
| } else if (!node->isWrapper() && !node->isMarkedReimp()) { |
| // Don't require documentation of things defined in Q_GADGET |
| if (node->name() != QLatin1String("QtGadgetHelper")) |
| node->location().warning( |
| tr("No documentation for '%1'").arg(node->plainSignature())); |
| } |
| } else if (!node->isSharingComment()) { |
| // Reimplements clause and type alias info precede body text |
| if (fn && !fn->overridesThis().isEmpty()) |
| generateReimplementsClause(fn, marker); |
| else if (node->isTypeAlias()) |
| generateAddendum(node, TypeAlias, marker, false); |
| |
| if (!generateText(node->doc().body(), node, marker)) { |
| if (node->isMarkedReimp()) |
| return; |
| } |
| |
| if (fn) { |
| if (fn->isQmlSignal()) |
| generateAddendum(node, QmlSignalHandler, marker); |
| if (fn->isPrivateSignal()) |
| generateAddendum(node, PrivateSignal, marker); |
| if (fn->isInvokable()) |
| generateAddendum(node, Invokable, marker); |
| if (fn->hasAssociatedProperties()) |
| generateAddendum(node, AssociatedProperties, marker); |
| } |
| |
| // Generate warnings |
| if (node->isEnumType()) { |
| const EnumNode *enume = static_cast<const EnumNode *>(node); |
| |
| QSet<QString> definedItems; |
| const QVector<EnumItem> &items = enume->items(); |
| for (const auto &item : items) |
| definedItems.insert(item.name()); |
| |
| const auto &documentedItemList = enume->doc().enumItemNames(); |
| QSet<QString> documentedItems(documentedItemList.cbegin(), documentedItemList.cend()); |
| const QSet<QString> allItems = definedItems + documentedItems; |
| if (allItems.count() > definedItems.count() |
| || allItems.count() > documentedItems.count()) { |
| for (const auto &it : allItems) { |
| if (!definedItems.contains(it)) { |
| QString details; |
| QString best = nearestName(it, definedItems); |
| if (!best.isEmpty() && !documentedItems.contains(best)) |
| details = tr("Maybe you meant '%1'?").arg(best); |
| |
| node->doc().location().warning(tr("No such enum item '%1' in %2") |
| .arg(it) |
| .arg(node->plainFullName()), |
| details); |
| } else if (!documentedItems.contains(it)) { |
| node->doc().location().warning(tr("Undocumented enum item '%1' in %2") |
| .arg(it) |
| .arg(node->plainFullName())); |
| } |
| } |
| } |
| } else if (fn) { |
| const QSet<QString> declaredNames = fn->parameters().getNames(); |
| const QSet<QString> documentedNames = fn->doc().parameterNames(); |
| if (declaredNames != documentedNames) { |
| for (const auto &name : declaredNames) { |
| if (!documentedNames.contains(name)) { |
| if (fn->isActive() || fn->isPreliminary()) { |
| if (!fn->isMarkedReimp() && !fn->isOverload()) { |
| fn->doc().location().warning(tr("Undocumented parameter '%1' in %2") |
| .arg(name) |
| .arg(node->plainFullName())); |
| } |
| } |
| } |
| } |
| for (const auto &name : documentedNames) { |
| if (!declaredNames.contains(name)) { |
| QString best = nearestName(name, declaredNames); |
| QString details; |
| if (!best.isEmpty()) |
| details = tr("Maybe you meant '%1'?").arg(best); |
| fn->doc().location().warning(tr("No such parameter '%1' in %2") |
| .arg(name) |
| .arg(fn->plainFullName()), |
| details); |
| } |
| } |
| } |
| /* |
| This return value check should be implemented |
| for all functions with a return type. |
| mws 13/12/2018 |
| */ |
| if (!fn->isObsolete() && fn->returnsBool() && !fn->isMarkedReimp() |
| && !fn->isOverload()) { |
| if (!fn->doc().body().contains("return")) |
| node->doc().location().warning( |
| tr("Undocumented return value " |
| "(hint: use 'return' or 'returns' in the text")); |
| } |
| } |
| } |
| generateRequiredLinks(node, marker); |
| } |
| |
| /*! |
| Generates either a link to the project folder for example \a node, or a list |
| of links files/images if 'url.examples config' variable is not defined. |
| |
| Does nothing for non-example nodes. |
| */ |
| void Generator::generateRequiredLinks(const Node *node, CodeMarker *marker) |
| { |
| if (!node->isExample()) |
| return; |
| |
| const ExampleNode *en = static_cast<const ExampleNode *>(node); |
| QString exampleUrl = Config::instance().getString(CONFIG_URL + Config::dot + CONFIG_EXAMPLES); |
| |
| if (exampleUrl.isEmpty()) { |
| if (!en->noAutoList()) { |
| generateFileList(en, marker, false); // files |
| generateFileList(en, marker, true); // images |
| } |
| } else { |
| generateLinkToExample(en, marker, exampleUrl); |
| } |
| } |
| |
| /*! |
| Generates an external link to the project folder for example \a node. |
| The path to the example replaces a placeholder '\1' character if |
| one is found in the \a baseUrl string. If no such placeholder is found, |
| the path is appended to \a baseUrl, after a '/' character if \a baseUrl did |
| not already end in one. |
| */ |
| void Generator::generateLinkToExample(const ExampleNode *en, CodeMarker *marker, |
| const QString &baseUrl) |
| { |
| QString exampleUrl(baseUrl); |
| QString link; |
| #ifndef QT_BOOTSTRAPPED |
| link = QUrl(exampleUrl).host(); |
| #endif |
| if (!link.isEmpty()) |
| link.prepend(" @ "); |
| link.prepend("Example project"); |
| |
| const QLatin1Char separator('/'); |
| const QLatin1Char placeholder('\1'); |
| if (!exampleUrl.contains(placeholder)) { |
| if (!exampleUrl.endsWith(separator)) |
| exampleUrl += separator; |
| exampleUrl += placeholder; |
| } |
| |
| // Construct a path to the example; <install path>/<example name> |
| QString pathRoot = en->doc().metaTagMap().value(QLatin1String("installpath")); |
| if (pathRoot.isEmpty()) |
| pathRoot = Config::instance().getString(CONFIG_EXAMPLESINSTALLPATH); |
| QStringList path = QStringList() << pathRoot << en->name(); |
| path.removeAll({}); |
| |
| Text text; |
| text << Atom::ParaLeft |
| << Atom(Atom::Link, exampleUrl.replace(placeholder, path.join(separator))) |
| << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << Atom(Atom::String, link) |
| << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom::ParaRight; |
| |
| generateText(text, nullptr, marker); |
| } |
| |
| void Generator::addImageToCopy(const ExampleNode *en, const QString &file) |
| { |
| QDir dirInfo; |
| QString userFriendlyFilePath; |
| const QString prefix("/images/used-in-examples/"); |
| QString srcPath = Config::findFile(en->location(), QStringList(), exampleDirs, file, |
| exampleImgExts, &userFriendlyFilePath); |
| outFileNames_ << prefix.mid(1) + userFriendlyFilePath; |
| userFriendlyFilePath.truncate(userFriendlyFilePath.lastIndexOf('/')); |
| QString imgOutDir = outDir_ + prefix + userFriendlyFilePath; |
| if (!dirInfo.mkpath(imgOutDir)) |
| en->location().fatal(tr("Cannot create output directory '%1'").arg(imgOutDir)); |
| Config::copyFile(en->location(), srcPath, file, imgOutDir); |
| } |
| |
| /*! |
| This function is called when the documentation for an example is |
| being formatted. It outputs a list of files for the example, which |
| can be the example's source files or the list of images used by the |
| example. The images are copied into a subtree of |
| \c{...doc/html/images/used-in-examples/...} |
| */ |
| void Generator::generateFileList(const ExampleNode *en, CodeMarker *marker, bool images) |
| { |
| Text text; |
| OpenedList openedList(OpenedList::Bullet); |
| QString tag; |
| QStringList paths; |
| Atom::AtomType atomType = Atom::ExampleFileLink; |
| |
| if (images) { |
| paths = en->images(); |
| tag = "Images:"; |
| atomType = Atom::ExampleImageLink; |
| } else { // files |
| paths = en->files(); |
| tag = "Files:"; |
| } |
| std::sort(paths.begin(), paths.end(), Generator::comparePaths); |
| |
| text << Atom::ParaLeft << tag << Atom::ParaRight; |
| text << Atom(Atom::ListLeft, openedList.styleString()); |
| |
| QString path; |
| for (const auto &file : qAsConst(paths)) { |
| if (images) { |
| if (!file.isEmpty()) |
| addImageToCopy(en, file); |
| } else { |
| generateExampleFilePage(en, file, marker); |
| } |
| |
| openedList.next(); |
| text << Atom(Atom::ListItemNumber, openedList.numberString()) |
| << Atom(Atom::ListItemLeft, openedList.styleString()) << Atom::ParaLeft |
| << Atom(atomType, file) << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << file |
| << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom::ParaRight |
| << Atom(Atom::ListItemRight, openedList.styleString()); |
| path = file; |
| } |
| text << Atom(Atom::ListRight, openedList.styleString()); |
| if (!paths.isEmpty()) |
| generateText(text, en, marker); |
| } |
| |
| void Generator::generateInheritedBy(const ClassNode *classe, CodeMarker *marker) |
| { |
| if (!classe->derivedClasses().isEmpty()) { |
| Text text; |
| text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) |
| << "Inherited by: " << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD); |
| |
| appendSortedNames(text, classe, classe->derivedClasses()); |
| text << Atom::ParaRight; |
| generateText(text, classe, marker); |
| } |
| } |
| |
| void Generator::generateInherits(const ClassNode *classe, CodeMarker *marker) |
| { |
| if (!classe->baseClasses().isEmpty()) { |
| Text text; |
| text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) |
| << "Inherits: " << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD); |
| |
| int index = 0; |
| const QVector<RelatedClass> &baseClasses = classe->baseClasses(); |
| for (const auto &cls : baseClasses) { |
| if (cls.node_) { |
| appendFullName(text, cls.node_, classe); |
| |
| if (cls.access_ == Node::Protected) { |
| text << " (protected)"; |
| } else if (cls.access_ == Node::Private) { |
| text << " (private)"; |
| } |
| text << separator(index++, classe->baseClasses().count()); |
| } |
| } |
| text << Atom::ParaRight; |
| generateText(text, classe, marker); |
| } |
| } |
| |
| /*! |
| Recursive writing of HTML files from the root \a node. |
| */ |
| void Generator::generateDocumentation(Node *node) |
| { |
| if (!node->url().isNull()) |
| return; |
| if (node->isIndexNode()) |
| return; |
| if (node->isInternal() && !showInternal_) |
| return; |
| if (node->isExternalPage()) |
| return; |
| |
| /* |
| Obtain a code marker for the source file. |
| */ |
| CodeMarker *marker = CodeMarker::markerForFileName(node->location().filePath()); |
| |
| if (node->parent() != nullptr) { |
| if (node->isCollectionNode()) { |
| /* |
| A collection node collects: groups, C++ modules, |
| QML modules or JavaScript modules. Testing for a |
| CollectionNode must be done before testing for a |
| TextPageNode because a CollectionNode is a PageNode |
| at this point. |
| |
| Don't output an HTML page for the collection |
| node unless the \group, \module, \qmlmodule or |
| \jsmodule command was actually seen by qdoc in |
| the qdoc comment for the node. |
| |
| A key prerequisite in this case is the call to |
| mergeCollections(cn). We must determine whether |
| this group, module, QML module, or JavaScript |
| module has members in other modules. We know at |
| this point that cn's members list contains only |
| members in the current module. Therefore, before |
| outputting the page for cn, we must search for |
| members of cn in the other modules and add them |
| to the members list. |
| */ |
| CollectionNode *cn = static_cast<CollectionNode *>(node); |
| if (cn->wasSeen()) { |
| qdb_->mergeCollections(cn); |
| beginSubPage(node, fileName(node)); |
| generateCollectionNode(cn, marker); |
| endSubPage(); |
| } else if (cn->isGenericCollection()) { |
| // Currently used only for the module's related orphans page |
| // but can be generalized for other kinds of collections if |
| // other use cases pop up. |
| QString name = cn->name().toLower(); |
| name.replace(QChar(' '), QString("-")); |
| QString filename = |
| cn->tree()->physicalModuleName() + "-" + name + "." + fileExtension(); |
| beginSubPage(node, filename); |
| generateGenericCollectionPage(cn, marker); |
| endSubPage(); |
| } |
| } else if (node->isTextPageNode()) { |
| beginSubPage(node, fileName(node)); |
| generatePageNode(static_cast<PageNode *>(node), marker); |
| endSubPage(); |
| } else if (node->isAggregate()) { |
| if ((node->isClassNode() || node->isHeader() || node->isNamespace()) |
| && node->docMustBeGenerated()) { |
| beginSubPage(node, fileName(node)); |
| generateCppReferencePage(static_cast<Aggregate *>(node), marker); |
| endSubPage(); |
| } else if (node->isQmlType() || node->isJsType()) { |
| beginSubPage(node, fileName(node)); |
| QmlTypeNode *qcn = static_cast<QmlTypeNode *>(node); |
| generateQmlTypePage(qcn, marker); |
| endSubPage(); |
| } else if (node->isQmlBasicType() || node->isJsBasicType()) { |
| beginSubPage(node, fileName(node)); |
| QmlBasicTypeNode *qbtn = static_cast<QmlBasicTypeNode *>(node); |
| generateQmlBasicTypePage(qbtn, marker); |
| endSubPage(); |
| } else if (node->isProxyNode()) { |
| beginSubPage(node, fileName(node)); |
| generateProxyPage(static_cast<Aggregate *>(node), marker); |
| endSubPage(); |
| } |
| } |
| } |
| |
| if (node->isAggregate()) { |
| Aggregate *aggregate = static_cast<Aggregate *>(node); |
| const NodeList &children = aggregate->childNodes(); |
| for (auto *node : children) { |
| if (node->isPageNode() && !node->isPrivate()) |
| generateDocumentation(node); |
| } |
| } |
| } |
| |
| /*! |
| Generate a list of maintainers in the output |
| */ |
| void Generator::generateMaintainerList(const Aggregate *node, CodeMarker *marker) |
| { |
| QStringList sl = getMetadataElements(node, "maintainer"); |
| |
| if (!sl.isEmpty()) { |
| Text text; |
| text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) |
| << "Maintained by: " << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD); |
| |
| for (int i = 0; i < sl.size(); ++i) |
| text << sl.at(i) << separator(i, sl.size()); |
| |
| text << Atom::ParaRight; |
| generateText(text, node, marker); |
| } |
| } |
| |
| /*! |
| Output the "Inherit by" list for the QML element, |
| if it is inherited by any other elements. |
| */ |
| void Generator::generateQmlInheritedBy(const QmlTypeNode *qcn, CodeMarker *marker) |
| { |
| if (qcn) { |
| NodeList subs; |
| QmlTypeNode::subclasses(qcn, subs); |
| if (!subs.isEmpty()) { |
| Text text; |
| text << Atom::ParaLeft << "Inherited by "; |
| appendSortedQmlNames(text, qcn, subs); |
| text << Atom::ParaRight; |
| generateText(text, qcn, marker); |
| } |
| } |
| } |
| |
| /*! |
| Extract sections of markup text surrounded by \e qmltext |
| and \e endqmltext and output them. |
| */ |
| bool Generator::generateQmlText(const Text &text, const Node *relative, CodeMarker *marker, |
| const QString & /* qmlName */) |
| { |
| const Atom *atom = text.firstAtom(); |
| bool result = false; |
| |
| if (atom != nullptr) { |
| initializeTextOutput(); |
| while (atom) { |
| if (atom->type() != Atom::QmlText) |
| atom = atom->next(); |
| else { |
| atom = atom->next(); |
| while (atom && (atom->type() != Atom::EndQmlText)) { |
| int n = 1 + generateAtom(atom, relative, marker); |
| while (n-- > 0) |
| atom = atom->next(); |
| } |
| } |
| } |
| result = true; |
| } |
| return result; |
| } |
| |
| void Generator::generateReimplementsClause(const FunctionNode *fn, CodeMarker *marker) |
| { |
| if (!fn->overridesThis().isEmpty()) { |
| if (fn->parent()->isClassNode()) { |
| ClassNode *cn = static_cast<ClassNode *>(fn->parent()); |
| const FunctionNode *overrides = cn->findOverriddenFunction(fn); |
| if (overrides && !overrides->isPrivate() && !overrides->parent()->isPrivate()) { |
| if (overrides->hasDoc()) { |
| Text text; |
| text << Atom::ParaLeft << "Reimplements: "; |
| QString fullName = |
| overrides->parent()->name() + "::" + overrides->signature(false, true); |
| appendFullName(text, overrides->parent(), fullName, overrides); |
| text << "." << Atom::ParaRight; |
| generateText(text, fn, marker); |
| } else { |
| fn->doc().location().warning( |
| tr("Illegal \\reimp; no documented virtual function for %1") |
| .arg(overrides->plainSignature())); |
| } |
| return; |
| } |
| const PropertyNode *sameName = cn->findOverriddenProperty(fn); |
| if (sameName && sameName->hasDoc()) { |
| Text text; |
| text << Atom::ParaLeft << "Reimplements an access function for property: "; |
| QString fullName = sameName->parent()->name() + "::" + sameName->name(); |
| appendFullName(text, sameName->parent(), fullName, sameName); |
| text << "." << Atom::ParaRight; |
| generateText(text, fn, marker); |
| } |
| } |
| } |
| } |
| |
| QString Generator::formatSince(const Node *node) |
| { |
| QStringList since = node->since().split(QLatin1Char(' ')); |
| |
| // If there is only one argument, assume it is the Qt version number. |
| if (since.count() == 1) |
| return "Qt " + since[0]; |
| |
| // Otherwise, use the original <project> <version> string. |
| return node->since(); |
| } |
| |
| void Generator::generateSince(const Node *node, CodeMarker *marker) |
| { |
| if (!node->since().isEmpty()) { |
| Text text; |
| text << Atom::ParaLeft << "This " << typeString(node) << " was introduced "; |
| if (node->isEnumType()) |
| text << "or modified "; |
| text << "in " << formatSince(node) << "." << Atom::ParaRight; |
| generateText(text, node, marker); |
| } |
| } |
| |
| void Generator::generateStatus(const Node *node, CodeMarker *marker) |
| { |
| Text text; |
| |
| switch (node->status()) { |
| case Node::Active: |
| // Do nothing. |
| break; |
| case Node::Preliminary: |
| text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) << "This " |
| << typeString(node) << " is under development and is subject to change." |
| << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << Atom::ParaRight; |
| break; |
| case Node::Deprecated: |
| text << Atom::ParaLeft; |
| if (node->isAggregate()) |
| text << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD); |
| text << "This " << typeString(node) << " is deprecated."; |
| if (node->isAggregate()) |
| text << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD); |
| text << Atom::ParaRight; |
| break; |
| case Node::Obsolete: |
| text << Atom::ParaLeft; |
| if (node->isAggregate()) |
| text << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD); |
| text << "This " << typeString(node) << " is obsolete."; |
| if (node->isAggregate()) |
| text << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD); |
| text << " It is provided to keep old source code working. " |
| << "We strongly advise against " |
| << "using it in new code." << Atom::ParaRight; |
| break; |
| case Node::Internal: |
| default: |
| break; |
| } |
| generateText(text, node, marker); |
| } |
| |
| /*! |
| Generates an addendum note of type \a type for \a node, using \a marker |
| as the code marker. |
| */ |
| void Generator::generateAddendum(const Node *node, Addendum type, CodeMarker *marker, |
| bool generateNote) |
| { |
| Q_ASSERT(node && !node->name().isEmpty()); |
| Text text; |
| text << Atom::ParaLeft; |
| |
| if (generateNote) { |
| text << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) |
| << "Note: " << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD); |
| } |
| |
| switch (type) { |
| case Invokable: |
| text << "This function can be invoked via the meta-object system and from QML. See " |
| << Atom(Atom::Link, "Q_INVOKABLE") |
| << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << "Q_INVOKABLE" |
| << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << "."; |
| break; |
| case PrivateSignal: |
| text << "This is a private signal. It can be used in signal connections " |
| "but cannot be emitted by the user."; |
| break; |
| case QmlSignalHandler: |
| { |
| QString handler(node->name()); |
| handler[0] = handler[0].toTitleCase(); |
| handler.prepend(QLatin1String("on")); |
| text << "The corresponding handler is " |
| << Atom(Atom::FormattingLeft, ATOM_FORMATTING_TELETYPE) << handler |
| << Atom(Atom::FormattingRight, ATOM_FORMATTING_TELETYPE) << "."; |
| break; |
| } |
| case AssociatedProperties: |
| { |
| if (!node->isFunction()) |
| return; |
| const FunctionNode *fn = static_cast<const FunctionNode *>(node); |
| NodeList nodes = fn->associatedProperties(); |
| if (nodes.isEmpty()) |
| return; |
| std::sort(nodes.begin(), nodes.end(), Node::nodeNameLessThan); |
| for (const auto *n : qAsConst(nodes)) { |
| QString msg; |
| const PropertyNode *pn = static_cast<const PropertyNode *>(n); |
| switch (pn->role(fn)) { |
| case PropertyNode::Getter: |
| msg = QStringLiteral("Getter function"); |
| break; |
| case PropertyNode::Setter: |
| msg = QStringLiteral("Setter function"); |
| break; |
| case PropertyNode::Resetter: |
| msg = QStringLiteral("Resetter function"); |
| break; |
| case PropertyNode::Notifier: |
| msg = QStringLiteral("Notifier signal"); |
| break; |
| default: |
| continue; |
| } |
| text << msg << " for property " << Atom(Atom::Link, pn->name()) |
| << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << pn->name() |
| << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << ". "; |
| } |
| break; |
| } |
| case TypeAlias: |
| { |
| if (!node->isTypeAlias()) |
| return; |
| const auto *ta = static_cast<const TypeAliasNode *>(node); |
| text << "This is a type alias for "; |
| if (ta->aliasedNode() && ta->aliasedNode()->isInAPI()) { |
| text << Atom(Atom::LinkNode, CodeMarker::stringForNode(ta->aliasedNode())) |
| << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) |
| << Atom(Atom::String, ta->aliasedNode()->plainFullName(ta->parent())) |
| << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << "."; |
| } else { |
| text << Atom(Atom::String, ta->aliasedType()) << "."; |
| } |
| break; |
| } |
| default: |
| return; |
| } |
| |
| text << Atom::ParaRight; |
| generateText(text, node, marker); |
| } |
| |
| /*! |
| Generate the documentation for \a relative. i.e. \a relative |
| is the node that represents the entity where a qdoc comment |
| was found, and \a text represents the qdoc comment. |
| */ |
| bool Generator::generateText(const Text &text, const Node *relative, CodeMarker *marker) |
| { |
| bool result = false; |
| if (text.firstAtom() != nullptr) { |
| int numAtoms = 0; |
| initializeTextOutput(); |
| generateAtomList(text.firstAtom(), relative, marker, true, numAtoms); |
| result = true; |
| } |
| return result; |
| } |
| |
| /* |
| The node is an aggregate, typically a class node, which has |
| a threadsafeness level. This function checks all the children |
| of the node to see if they are exceptions to the node's |
| threadsafeness. If there are any exceptions, the exceptions |
| are added to the appropriate set (reentrant, threadsafe, and |
| nonreentrant, and true is returned. If there are no exceptions, |
| the three node lists remain empty and false is returned. |
| */ |
| bool Generator::hasExceptions(const Node *node, NodeList &reentrant, NodeList &threadsafe, |
| NodeList &nonreentrant) |
| { |
| bool result = false; |
| Node::ThreadSafeness ts = node->threadSafeness(); |
| const NodeList &children = static_cast<const Aggregate *>(node)->childNodes(); |
| for (auto child : children) { |
| if (!child->isObsolete()) { |
| switch (child->threadSafeness()) { |
| case Node::Reentrant: |
| reentrant.append(child); |
| if (ts == Node::ThreadSafe) |
| result = true; |
| break; |
| case Node::ThreadSafe: |
| threadsafe.append(child); |
| if (ts == Node::Reentrant) |
| result = true; |
| break; |
| case Node::NonReentrant: |
| nonreentrant.append(child); |
| result = true; |
| break; |
| default: |
| break; |
| } |
| } |
| } |
| return result; |
| } |
| |
| static void startNote(Text &text) |
| { |
| text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) |
| << "Note:" << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << " "; |
| } |
| |
| /*! |
| Generates text that explains how threadsafe and/or reentrant |
| \a node is. |
| */ |
| void Generator::generateThreadSafeness(const Node *node, CodeMarker *marker) |
| { |
| Text text, rlink, tlink; |
| NodeList reentrant; |
| NodeList threadsafe; |
| NodeList nonreentrant; |
| Node::ThreadSafeness ts = node->threadSafeness(); |
| bool exceptions = false; |
| |
| rlink << Atom(Atom::Link, "reentrant") << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) |
| << "reentrant" << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); |
| |
| tlink << Atom(Atom::Link, "thread-safe") << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) |
| << "thread-safe" << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); |
| |
| switch (ts) { |
| case Node::UnspecifiedSafeness: |
| break; |
| case Node::NonReentrant: |
| text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) |
| << "Warning:" << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << " This " |
| << typeString(node) << " is not " << rlink << "." << Atom::ParaRight; |
| break; |
| case Node::Reentrant: |
| case Node::ThreadSafe: |
| startNote(text); |
| if (node->isAggregate()) { |
| exceptions = hasExceptions(node, reentrant, threadsafe, nonreentrant); |
| text << "All functions in this " << typeString(node) << " are "; |
| if (ts == Node::ThreadSafe) |
| text << tlink; |
| else |
| text << rlink; |
| |
| if (!exceptions || (ts == Node::Reentrant && !threadsafe.isEmpty())) |
| text << "."; |
| else |
| text << " with the following exceptions:"; |
| } else { |
| text << "This " << typeString(node) << " is "; |
| if (ts == Node::ThreadSafe) |
| text << tlink; |
| else |
| text << rlink; |
| text << "."; |
| } |
| text << Atom::ParaRight; |
| break; |
| default: |
| break; |
| } |
| generateText(text, node, marker); |
| |
| if (exceptions) { |
| text.clear(); |
| if (ts == Node::Reentrant) { |
| if (!nonreentrant.isEmpty()) { |
| startNote(text); |
| text << "These functions are not " << rlink << ":" << Atom::ParaRight; |
| signatureList(nonreentrant, node, marker); |
| } |
| if (!threadsafe.isEmpty()) { |
| text.clear(); |
| startNote(text); |
| text << "These functions are also " << tlink << ":" << Atom::ParaRight; |
| generateText(text, node, marker); |
| signatureList(threadsafe, node, marker); |
| } |
| } else { // thread-safe |
| if (!reentrant.isEmpty()) { |
| startNote(text); |
| text << "These functions are only " << rlink << ":" << Atom::ParaRight; |
| signatureList(reentrant, node, marker); |
| } |
| if (!nonreentrant.isEmpty()) { |
| text.clear(); |
| startNote(text); |
| text << "These functions are not " << rlink << ":" << Atom::ParaRight; |
| signatureList(nonreentrant, node, marker); |
| } |
| } |
| } |
| } |
| |
| /*! |
| Returns the string containing an example code of the input node, |
| if it is an overloaded signal. Otherwise, returns an empty string. |
| */ |
| QString Generator::getOverloadedSignalCode(const Node *node) |
| { |
| if (!node->isFunction()) |
| return QString(); |
| const auto func = static_cast<const FunctionNode *>(node); |
| if (!func->isSignal() || !func->hasOverloads()) |
| return QString(); |
| |
| // Compute a friendly name for the object of that instance. |
| // e.g: "QAbstractSocket" -> "abstractSocket" |
| QString objectName = node->parent()->name(); |
| if (objectName.size() >= 2) { |
| if (objectName[0] == 'Q') |
| objectName = objectName.mid(1); |
| objectName[0] = objectName[0].toLower(); |
| } |
| |
| // We have an overloaded signal, show an example. Note, for const |
| // overloaded signals, one should use Q{Const,NonConst}Overload, but |
| // it is very unlikely that we will ever have public API overloading |
| // signals by const. |
| QString code = "connect(" + objectName + ", QOverload<"; |
| code += func->parameters().generateTypeList(); |
| code += ">::of(&" + func->parent()->name() + "::" + func->name() + "),\n [=]("; |
| code += func->parameters().generateTypeAndNameList(); |
| code += "){ /* ... */ });"; |
| |
| return code; |
| } |
| |
| /*! |
| If the node is an overloaded signal, add a node with an example on how to connect to it |
| */ |
| void Generator::generateOverloadedSignal(const Node *node, CodeMarker *marker) |
| { |
| QString code = getOverloadedSignalCode(node); |
| if (code.isEmpty()) |
| return; |
| |
| Text text; |
| text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) |
| << "Note:" << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << " Signal " |
| << Atom(Atom::FormattingLeft, ATOM_FORMATTING_ITALIC) << node->name() |
| << Atom(Atom::FormattingRight, ATOM_FORMATTING_ITALIC) |
| << " is overloaded in this class. " |
| "To connect to this signal by using the function pointer syntax, Qt " |
| "provides a convenient helper for obtaining the function pointer as " |
| "shown in this example:" |
| << Atom(Atom::Code, marker->markedUpCode(code, node, node->location())); |
| |
| generateText(text, node, marker); |
| } |
| |
| /*! |
| Traverses the database recursively to generate all the documentation. |
| */ |
| void Generator::generateDocs() |
| { |
| currentGenerator_ = this; |
| generateDocumentation(qdb_->primaryTreeRoot()); |
| } |
| |
| Generator *Generator::generatorForFormat(const QString &format) |
| { |
| for (const auto &generator : qAsConst(generators)) { |
| if (generator->format() == format) |
| return generator; |
| } |
| return nullptr; |
| } |
| |
| /*! |
| Looks up the tag \a tag in the map of metadata values for the |
| current topic in \a inner. If a value for the tag is found, |
| the value is returned. |
| |
| \note If \a tag is found in the metadata map, it is erased. |
| i.e. Once you call this function for a particular \a tag, |
| you consume \a tag. |
| */ |
| QString Generator::getMetadataElement(const Aggregate *inner, const QString &tag) |
| { |
| QString s; |
| QStringMultiMap &metaTagMap = const_cast<QStringMultiMap &>(inner->doc().metaTagMap()); |
| for (auto it = metaTagMap.find(tag); it != metaTagMap.end();) { |
| s = it.value(); |
| metaTagMap.erase(it); |
| } |
| return s; |
| } |
| |
| /*! |
| Looks up the tag \a t in the map of metadata values for the |
| current topic in \a inner. If values for the tag are found, |
| they are returned in a string list. |
| |
| \note If \a t is found in the metadata map, all the pairs |
| having the key \a t are erased. i.e. Once you call this |
| function for a particular \a t, you consume \a t. |
| */ |
| QStringList Generator::getMetadataElements(const Aggregate *inner, const QString &t) |
| { |
| QStringList s; |
| QStringMultiMap &metaTagMap = const_cast<QStringMultiMap &>(inner->doc().metaTagMap()); |
| s = metaTagMap.values(t); |
| if (!s.isEmpty()) |
| metaTagMap.remove(t); |
| return s; |
| } |
| |
| /*! |
| Returns a relative path name for an image. |
| */ |
| QString Generator::imageFileName(const Node *relative, const QString &fileBase) |
| { |
| QString userFriendlyFilePath; |
| QString filePath = Config::findFile(relative->doc().location(), imageFiles, imageDirs, fileBase, |
| imgFileExts[format()], &userFriendlyFilePath); |
| |
| if (filePath.isEmpty()) |
| return QString(); |
| |
| QString path = Config::copyFile(relative->doc().location(), filePath, userFriendlyFilePath, |
| outputDir() + QLatin1String("/images")); |
| int images_slash = path.lastIndexOf("images/"); |
| QString relImagePath; |
| if (images_slash != -1) |
| relImagePath = path.mid(images_slash); |
| return relImagePath; |
| } |
| |
| QString Generator::indent(int level, const QString &markedCode) |
| { |
| if (level == 0) |
| return markedCode; |
| |
| QString t; |
| int column = 0; |
| |
| int i = 0; |
| while (i < markedCode.length()) { |
| if (markedCode.at(i) == QLatin1Char('\n')) { |
| column = 0; |
| } else { |
| if (column == 0) { |
| for (int j = 0; j < level; j++) |
| t += QLatin1Char(' '); |
| } |
| column++; |
| } |
| t += markedCode.at(i++); |
| } |
| return t; |
| } |
| |
| void Generator::initialize() |
| { |
| Config &config = Config::instance(); |
| outputFormats = config.getOutputFormats(); |
| redirectDocumentationToDevNull_ = config.getBool(CONFIG_REDIRECTDOCUMENTATIONTODEVNULL); |
| |
| imageFiles = config.getCanonicalPathList(CONFIG_IMAGES); |
| imageDirs = config.getCanonicalPathList(CONFIG_IMAGEDIRS); |
| scriptFiles = config.getCanonicalPathList(CONFIG_SCRIPTS); |
| scriptDirs = config.getCanonicalPathList(CONFIG_SCRIPTDIRS); |
| styleFiles = config.getCanonicalPathList(CONFIG_STYLES); |
| styleDirs = config.getCanonicalPathList(CONFIG_STYLEDIRS); |
| exampleDirs = config.getCanonicalPathList(CONFIG_EXAMPLEDIRS); |
| exampleImgExts = config.getStringList(CONFIG_EXAMPLES + Config::dot + CONFIG_IMAGEEXTENSIONS); |
| |
| QString imagesDotFileExtensions = CONFIG_IMAGES + Config::dot + CONFIG_FILEEXTENSIONS; |
| for (const auto &ext : config.subVars(imagesDotFileExtensions)) |
| imgFileExts[ext] = config.getStringList(imagesDotFileExtensions + Config::dot + ext); |
| |
| for (auto &g : generators) { |
| if (outputFormats.contains(g->format())) { |
| currentGenerator_ = g; |
| g->initializeGenerator(); |
| } |
| } |
| |
| for (const auto &n : config.subVars(CONFIG_FORMATTING)) { |
| QString formattingDotName = CONFIG_FORMATTING + Config::dot + n; |
| for (const auto &f : config.subVars(formattingDotName)) { |
| QString def = config.getString(formattingDotName + Config::dot + f); |
| if (!def.isEmpty()) { |
| int numParams = Config::numParams(def); |
| int numOccs = def.count("\1"); |
| if (numParams != 1) { |
| config.lastLocation().warning(tr("Formatting '%1' must " |
| "have exactly one " |
| "parameter (found %2)") |
| .arg(n) |
| .arg(numParams)); |
| } else if (numOccs > 1) { |
| config.lastLocation().fatal(tr("Formatting '%1' must " |
| "contain exactly one " |
| "occurrence of '\\1' " |
| "(found %2)") |
| .arg(n) |
| .arg(numOccs)); |
| } else { |
| int paramPos = def.indexOf("\1"); |
| fmtLeftMaps[f].insert(n, def.left(paramPos)); |
| fmtRightMaps[f].insert(n, def.mid(paramPos + 1)); |
| } |
| } |
| } |
| } |
| |
| project_ = config.getString(CONFIG_PROJECT); |
| outDir_ = config.getOutputDir(); |
| outSubdir_ = outDir_.mid(outDir_.lastIndexOf('/') + 1); |
| |
| outputPrefixes.clear(); |
| QStringList items = config.getStringList(CONFIG_OUTPUTPREFIXES); |
| if (!items.isEmpty()) { |
| for (const auto &prefix : items) |
| outputPrefixes[prefix] = config.getString(CONFIG_OUTPUTPREFIXES + Config::dot + prefix); |
| } else { |
| outputPrefixes[QLatin1String("QML")] = QLatin1String("qml-"); |
| outputPrefixes[QLatin1String("JS")] = QLatin1String("js-"); |
| } |
| |
| outputSuffixes.clear(); |
| for (const auto &suffix : config.getStringList(CONFIG_OUTPUTSUFFIXES)) |
| outputSuffixes[suffix] = config.getString(CONFIG_OUTPUTSUFFIXES + Config::dot + suffix); |
| |
| noLinkErrors_ = config.getBool(CONFIG_NOLINKERRORS); |
| autolinkErrors_ = config.getBool(CONFIG_AUTOLINKERRORS); |
| } |
| |
| /*! |
| Creates template-specific subdirs (e.g. /styles and /scripts for HTML) |
| and copies the files to them. |
| */ |
| void Generator::copyTemplateFiles(const QString &configVar, const QString &subDir) |
| { |
| Config &config = Config::instance(); |
| QStringList files = config.getCanonicalPathList(configVar, true); |
| if (!files.isEmpty()) { |
| QDir dirInfo; |
| QString templateDir = outDir_ + QLatin1Char('/') + subDir; |
| if (!dirInfo.exists(templateDir) && !dirInfo.mkdir(templateDir)) { |
| config.lastLocation().fatal( |
| tr("Cannot create %1 directory '%2'").arg(subDir, templateDir)); |
| } else { |
| for (const auto &file : files) { |
| if (!file.isEmpty()) |
| Config::copyFile(config.lastLocation(), file, file, templateDir); |
| } |
| } |
| } |
| } |
| |
| /*! |
| Reads format-specific variables from config, sets output |
| (sub)directories, creates them on the filesystem and copies the |
| template-specific files. |
| */ |
| void Generator::initializeFormat() |
| { |
| Config &config = Config::instance(); |
| outFileNames_.clear(); |
| useOutputSubdirs_ = true; |
| if (config.getBool(format() + Config::dot + "nosubdirs")) |
| resetUseOutputSubdirs(); |
| |
| if (outputFormats.isEmpty()) |
| return; |
| |
| outDir_ = config.getOutputDir(format()); |
| if (outDir_.isEmpty()) { |
| config.lastLocation().fatal(tr("No output directory specified in " |
| "configuration file or on the command line")); |
| } else { |
| outSubdir_ = outDir_.mid(outDir_.lastIndexOf('/') + 1); |
| } |
| |
| QDir dirInfo; |
| if (dirInfo.exists(outDir_)) { |
| if (!config.generating() && Generator::useOutputSubdirs()) { |
| if (!Config::removeDirContents(outDir_)) |
| config.lastLocation().error(tr("Cannot empty output directory '%1'").arg(outDir_)); |
| } |
| } else if (!dirInfo.mkpath(outDir_)) { |
| config.lastLocation().fatal(tr("Cannot create output directory '%1'").arg(outDir_)); |
| } |
| |
| // Output directory exists, which is enough for prepare phase. |
| if (config.preparing()) |
| return; |
| |
| if (!dirInfo.exists(outDir_ + "/images") && !dirInfo.mkdir(outDir_ + "/images")) |
| config.lastLocation().fatal( |
| tr("Cannot create images directory '%1'").arg(outDir_ + "/images")); |
| |
| copyTemplateFiles(format() + Config::dot + CONFIG_STYLESHEETS, "style"); |
| copyTemplateFiles(format() + Config::dot + CONFIG_SCRIPTS, "scripts"); |
| copyTemplateFiles(format() + Config::dot + CONFIG_EXTRAIMAGES, "images"); |
| |
| // Use a format-specific .quotinginformation if defined, otherwise a global value |
| if (config.subVars(format()).contains(CONFIG_QUOTINGINFORMATION)) |
| quoting_ = config.getBool(format() + Config::dot + CONFIG_QUOTINGINFORMATION); |
| else |
| quoting_ = config.getBool(CONFIG_QUOTINGINFORMATION); |
| } |
| |
| /*! |
| Appends each directory path in \a moreImageDirs to the |
| list of image directories. |
| */ |
| void Generator::augmentImageDirs(QSet<QString> &moreImageDirs) |
| { |
| if (moreImageDirs.isEmpty()) |
| return; |
| for (const auto &it : moreImageDirs) |
| imageDirs.append(it); |
| } |
| |
| /*! |
| Sets the generator's pointer to the Config instance. |
| */ |
| void Generator::initializeGenerator() |
| { |
| showInternal_ = Config::instance().getBool(CONFIG_SHOWINTERNAL); |
| singleExec_ = Config::instance().getBool(CONFIG_SINGLEEXEC); |
| } |
| |
| bool Generator::matchAhead(const Atom *atom, Atom::AtomType expectedAtomType) |
| { |
| return atom->next() && atom->next()->type() == expectedAtomType; |
| } |
| |
| /*! |
| Used for writing to the current output stream. Returns a |
| reference to the current output stream, which is then used |
| with the \c {<<} operator for writing. |
| */ |
| QTextStream &Generator::out() |
| { |
| return *outStreamStack.top(); |
| } |
| |
| QString Generator::outFileName() |
| { |
| return QFileInfo(static_cast<QFile *>(out().device())->fileName()).fileName(); |
| } |
| |
| QString Generator::outputPrefix(const Node *node) |
| { |
| // Prefix is applied to QML and JS types |
| if (node->isQmlType() || node->isQmlBasicType()) |
| return outputPrefixes[QLatin1String("QML")]; |
| if (node->isJsType() || node->isJsBasicType()) |
| return outputPrefixes[QLatin1String("JS")]; |
| return QString(); |
| } |
| |
| QString Generator::outputSuffix(const Node *node) |
| { |
| // Suffix is applied to QML and JS types, as |
| // well as module pages. |
| if (node->isQmlModule() || node->isQmlType() || node->isQmlBasicType()) |
| return outputSuffixes[QLatin1String("QML")]; |
| if (node->isJsModule() || node->isJsType() || node->isJsBasicType()) |
| return outputSuffixes[QLatin1String("JS")]; |
| return QString(); |
| } |
| |
| bool Generator::parseArg(const QString &src, const QString &tag, int *pos, int n, |
| QStringRef *contents, QStringRef *par1, bool debug) |
| { |
| #define SKIP_CHAR(c) \ |
| if (debug) \ |
| qDebug() << "looking for " << c << " at " << QString(src.data() + i, n - i); \ |
| if (i >= n || src[i] != c) { \ |
| if (debug) \ |
| qDebug() << " char '" << c << "' not found"; \ |
| return false; \ |
| } \ |
| ++i; |
| |
| #define SKIP_SPACE \ |
| while (i < n && src[i] == ' ') \ |
| ++i; |
| |
| int i = *pos; |
| int j = i; |
| |
| // assume "<@" has been parsed outside |
| // SKIP_CHAR('<'); |
| // SKIP_CHAR('@'); |
| |
| if (tag != QStringRef(&src, i, tag.length())) { |
| return false; |
| } |
| |
| if (debug) |
| qDebug() << "haystack:" << src << "needle:" << tag << "i:" << i; |
| |
| // skip tag |
| i += tag.length(); |
| |
| // parse stuff like: linkTag("(<@link node=\"([^\"]+)\">).*(</@link>)"); |
| if (par1) { |
| SKIP_SPACE; |
| // read parameter name |
| j = i; |
| while (i < n && src[i].isLetter()) |
| ++i; |
| if (src[i] == '=') { |
| if (debug) |
| qDebug() << "read parameter" << QString(src.data() + j, i - j); |
| SKIP_CHAR('='); |
| SKIP_CHAR('"'); |
| // skip parameter name |
| j = i; |
| while (i < n && src[i] != '"') |
| ++i; |
| *par1 = QStringRef(&src, j, i - j); |
| SKIP_CHAR('"'); |
| SKIP_SPACE; |
| } else { |
| if (debug) |
| qDebug() << "no optional parameter found"; |
| } |
| } |
| SKIP_SPACE; |
| SKIP_CHAR('>'); |
| |
| // find contents up to closing "</@tag> |
| j = i; |
| for (; true; ++i) { |
| if (i + 4 + tag.length() > n) |
| return false; |
| if (src[i] != '<') |
| continue; |
| if (src[i + 1] != '/') |
| continue; |
| if (src[i + 2] != '@') |
| continue; |
| if (tag != QStringRef(&src, i + 3, tag.length())) |
| continue; |
| if (src[i + 3 + tag.length()] != '>') |
| continue; |
| break; |
| } |
| |
| *contents = QStringRef(&src, j, i - j); |
| |
| i += tag.length() + 4; |
| |
| *pos = i; |
| if (debug) |
| qDebug() << " tag " << tag << " found: pos now: " << i; |
| return true; |
| #undef SKIP_CHAR |
| } |
| |
| QString Generator::plainCode(const QString &markedCode) |
| { |
| QString t = markedCode; |
| t.replace(tag, QString()); |
| t.replace(quot, QLatin1String("\"")); |
| t.replace(gt, QLatin1String(">")); |
| t.replace(lt, QLatin1String("<")); |
| t.replace(amp, QLatin1String("&")); |
| return t; |
| } |
| |
| void Generator::setImageFileExtensions(const QStringList &extensions) |
| { |
| imgFileExts[format()] = extensions; |
| } |
| |
| void Generator::singularPlural(Text &text, const NodeList &nodes) |
| { |
| if (nodes.count() == 1) |
| text << " is"; |
| else |
| text << " are"; |
| } |
| |
| int Generator::skipAtoms(const Atom *atom, Atom::AtomType type) const |
| { |
| int skipAhead = 0; |
| atom = atom->next(); |
| while (atom && atom->type() != type) { |
| skipAhead++; |
| atom = atom->next(); |
| } |
| return skipAhead; |
| } |
| |
| /*! |
| Resets the variables used during text output. |
| */ |
| void Generator::initializeTextOutput() |
| { |
| inLink_ = false; |
| inContents_ = false; |
| inSectionHeading_ = false; |
| inTableHeader_ = false; |
| numTableRows_ = 0; |
| threeColumnEnumValueTable_ = true; |
| link_.clear(); |
| sectionNumber_.clear(); |
| } |
| |
| void Generator::supplementAlsoList(const Node *node, QVector<Text> &alsoList) |
| { |
| if (node->isFunction() && !node->isMacro()) { |
| const auto fn = static_cast<const FunctionNode *>(node); |
| if (fn->overloadNumber() == 0) { |
| QString alternateName; |
| const FunctionNode *alternateFunc = nullptr; |
| |
| if (fn->name().startsWith("set") && fn->name().size() >= 4) { |
| alternateName = fn->name()[3].toLower(); |
| alternateName += fn->name().mid(4); |
| alternateFunc = fn->parent()->findFunctionChild(alternateName, QString()); |
| |
| if (!alternateFunc) { |
| alternateName = "is" + fn->name().mid(3); |
| alternateFunc = fn->parent()->findFunctionChild(alternateName, QString()); |
| if (!alternateFunc) { |
| alternateName = "has" + fn->name().mid(3); |
| alternateFunc = fn->parent()->findFunctionChild(alternateName, QString()); |
| } |
| } |
| } else if (!fn->name().isEmpty()) { |
| alternateName = "set"; |
| alternateName += fn->name()[0].toUpper(); |
| alternateName += fn->name().mid(1); |
| alternateFunc = fn->parent()->findFunctionChild(alternateName, QString()); |
| } |
| |
| if (alternateFunc && alternateFunc->access() != Node::Private) { |
| int i; |
| for (i = 0; i < alsoList.size(); ++i) { |
| if (alsoList.at(i).toString().contains(alternateName)) |
| break; |
| } |
| |
| if (i == alsoList.size()) { |
| alternateName += "()"; |
| |
| Text also; |
| also << Atom(Atom::Link, alternateName) |
| << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << alternateName |
| << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); |
| alsoList.prepend(also); |
| } |
| } |
| } |
| } |
| } |
| |
| void Generator::terminate() |
| { |
| for (const auto &generator : qAsConst(generators)) { |
| if (outputFormats.contains(generator->format())) |
| generator->terminateGenerator(); |
| } |
| |
| fmtLeftMaps.clear(); |
| fmtRightMaps.clear(); |
| imgFileExts.clear(); |
| imageFiles.clear(); |
| imageDirs.clear(); |
| outDir_.clear(); |
| } |
| |
| void Generator::terminateGenerator() {} |
| |
| /*! |
| Trims trailing whitespace off the \a string and returns |
| the trimmed string. |
| */ |
| QString Generator::trimmedTrailing(const QString &string, const QString &prefix, |
| const QString &suffix) |
| { |
| QString trimmed = string; |
| while (trimmed.length() > 0 && trimmed[trimmed.length() - 1].isSpace()) |
| trimmed.truncate(trimmed.length() - 1); |
| |
| trimmed.append(suffix); |
| trimmed.prepend(prefix); |
| return trimmed; |
| } |
| |
| QString Generator::typeString(const Node *node) |
| { |
| switch (node->nodeType()) { |
| case Node::Namespace: |
| return "namespace"; |
| case Node::Class: |
| return "class"; |
| case Node::Struct: |
| return "struct"; |
| case Node::Union: |
| return "union"; |
| case Node::QmlType: |
| case Node::QmlBasicType: |
| case Node::JsBasicType: |
| return "type"; |
| case Node::Page: |
| return "documentation"; |
| case Node::Enum: |
| return "enum"; |
| case Node::Typedef: |
| return "typedef"; |
| case Node::TypeAlias: |
| return "alias"; |
| case Node::Function: { |
| const auto fn = static_cast<const FunctionNode *>(node); |
| switch (fn->metaness()) { |
| case FunctionNode::JsSignal: |
| case FunctionNode::QmlSignal: |
| return "signal"; |
| case FunctionNode::JsSignalHandler: |
| case FunctionNode::QmlSignalHandler: |
| return "signal handler"; |
| case FunctionNode::JsMethod: |
| case FunctionNode::QmlMethod: |
| return "method"; |
| default: |
| break; |
| } |
| return "function"; |
| } |
| case Node::Property: |
| case Node::QmlProperty: |
| return "property"; |
| case Node::Module: |
| case Node::JsModule: |
| case Node::QmlModule: |
| return "module"; |
| case Node::SharedComment: { |
| const auto &collective = static_cast<const SharedCommentNode *>(node)->collective(); |
| return collective.first()->nodeTypeString(); |
| } |
| default: |
| return "documentation"; |
| } |
| } |
| |
| void Generator::unknownAtom(const Atom *atom) |
| { |
| Location::internalError( |
| tr("unknown atom type '%1' in %2 generator").arg(atom->typeString()).arg(format())); |
| } |
| |
| QT_END_NAMESPACE |