diff options
Diffstat (limited to 'sources/shiboken6/generator/qtdoc')
-rw-r--r-- | sources/shiboken6/generator/qtdoc/qtdocgenerator.cpp | 1467 | ||||
-rw-r--r-- | sources/shiboken6/generator/qtdoc/qtdocgenerator.h | 121 | ||||
-rw-r--r-- | sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp | 763 | ||||
-rw-r--r-- | sources/shiboken6/generator/qtdoc/qtxmltosphinx.h | 83 | ||||
-rw-r--r-- | sources/shiboken6/generator/qtdoc/qtxmltosphinxinterface.h | 31 | ||||
-rw-r--r-- | sources/shiboken6/generator/qtdoc/rstformat.h | 55 |
6 files changed, 1539 insertions, 981 deletions
diff --git a/sources/shiboken6/generator/qtdoc/qtdocgenerator.cpp b/sources/shiboken6/generator/qtdoc/qtdocgenerator.cpp index 8c768a57d..2797ff254 100644 --- a/sources/shiboken6/generator/qtdoc/qtdocgenerator.cpp +++ b/sources/shiboken6/generator/qtdoc/qtdocgenerator.cpp @@ -1,33 +1,11 @@ -/**************************************************************************** -** -** Copyright (C) 2020 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of Qt for Python. -** -** $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$ -** -****************************************************************************/ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "qtdocgenerator.h" +#include "generatorcontext.h" +#include "codesnip.h" #include "exception.h" +#include "abstractmetaargument.h" #include "apiextractorresult.h" #include "qtxmltosphinx.h" #include "rstformat.h" @@ -37,6 +15,7 @@ #include <abstractmetafield.h> #include <abstractmetafunction.h> #include <abstractmetalang.h> +#include "abstractmetalang_helpers.h" #include <fileout.h> #include <messages.h> #include <modifications.h> @@ -44,25 +23,97 @@ #include <reporthandler.h> #include <textstream.h> #include <typedatabase.h> -#include <typesystem.h> +#include <functiontypeentry.h> +#include <enumtypeentry.h> +#include <complextypeentry.h> +#include <flagstypeentry.h> +#include <primitivetypeentry.h> #include <qtdocparser.h> #include <doxygenparser.h> +#include "qtcompat.h" + #include <QtCore/QTextStream> #include <QtCore/QFile> #include <QtCore/QDir> +#include <QtCore/QJsonArray> +#include <QtCore/QJsonDocument> +#include <QtCore/QJsonObject> +#include <QtCore/QSet> #include <algorithm> #include <limits> -static inline QString additionalDocumentationOption() { return QStringLiteral("additional-documentation"); } +using namespace Qt::StringLiterals; + +static inline QString classScope(const AbstractMetaClassCPtr &metaClass) +{ + return metaClass->fullName(); +} + +struct DocPackage +{ + QStringList classPages; + QStringList decoratorPages; + AbstractMetaFunctionCList globalFunctions; + AbstractMetaEnumList globalEnums; +}; + +struct DocGeneratorOptions +{ + QtXmlToSphinxParameters parameters; + QString extraSectionDir; + QString additionalDocumentationList; + QString inheritanceFile; + bool doxygen = false; + bool inheritanceDiagram = true; +}; + +struct GeneratorDocumentation +{ + struct Property + { + QString name; + Documentation documentation; + AbstractMetaType type; + AbstractMetaFunctionCPtr getter; + AbstractMetaFunctionCPtr setter; + AbstractMetaFunctionCPtr reset; + AbstractMetaFunctionCPtr notify; + }; + + AbstractMetaFunctionCList allFunctions; + AbstractMetaFunctionCList tocNormalFunctions; // Index lists + AbstractMetaFunctionCList tocVirtuals; + AbstractMetaFunctionCList tocSignalFunctions; + AbstractMetaFunctionCList tocSlotFunctions; + AbstractMetaFunctionCList tocStaticFunctions; + + QList<Property> properties; +}; + +static bool operator<(const GeneratorDocumentation::Property &lhs, + const GeneratorDocumentation::Property &rhs) +{ + return lhs.name < rhs.name; +} + +static QString propertyRefTarget(const QString &name) +{ + QString result = name; + // For sphinx referencing, disambiguate the target from the getter name + // by appending an invisible "Hangul choseong filler" character. + result.append(QChar(0x115F)); + return result; +} + +constexpr auto additionalDocumentationOption = "additional-documentation"_L1; -static inline QString none() { return QStringLiteral("None"); } +constexpr auto none = "None"_L1; static bool shouldSkip(const AbstractMetaFunctionCPtr &func) { - // Constructors go to separate section - if (DocParser::skipForQuery(func) || func->isConstructor()) + if (DocParser::skipForQuery(func)) return true; // Search a const clone (QImage::bits() vs QImage::bits() const) @@ -75,12 +126,12 @@ static bool shouldSkip(const AbstractMetaFunctionCPtr &func) if (f != func && f->isConstant() && f->name() == func->name() - && f->arguments().count() == funcArgs.count()) { + && f->arguments().size() == funcArgs.size()) { // Compare each argument bool cloneFound = true; const AbstractMetaArgumentList fargs = f->arguments(); - for (int i = 0, max = funcArgs.count(); i < max; ++i) { + for (qsizetype i = 0, max = funcArgs.size(); i < max; ++i) { if (funcArgs.at(i).type().typeEntry() != fargs.at(i).type().typeEntry()) { cloneFound = false; break; @@ -95,116 +146,205 @@ static bool shouldSkip(const AbstractMetaFunctionCPtr &func) static bool functionSort(const AbstractMetaFunctionCPtr &func1, const AbstractMetaFunctionCPtr &func2) { - return func1->name() < func2->name(); + const bool ctor1 = func1->isConstructor(); + if (ctor1 != func2->isConstructor()) + return ctor1; + const QString &name1 = func1->name(); + const QString &name2 = func2->name(); + if (name1 != name2) + return name1 < name2; + return func1->arguments().size() < func2->arguments().size(); } -static inline QVersionNumber versionOf(const TypeEntry *te) +static inline QVersionNumber versionOf(const TypeEntryCPtr &te) { if (te) { const auto version = te->version(); if (!version.isNull() && version > QVersionNumber(0, 0)) return version; } - return QVersionNumber(); -} - -static const QHash<QString, QString> &operatorMapping() -{ - static const QHash<QString, QString> result = { - {QLatin1String("operator+"), QLatin1String("__add__")}, - {QLatin1String("operator+="), QLatin1String("__iadd__")}, - {QLatin1String("operator-"), QLatin1String("__sub__")}, - {QLatin1String("operator-="), QLatin1String("__isub__")}, - {QLatin1String("operator*"), QLatin1String("__mul__")}, - {QLatin1String("operator*="), QLatin1String("__imul__")}, - {QLatin1String("operator/"), QLatin1String("__div__")}, - {QLatin1String("operator/="), QLatin1String("__idiv__")}, - {QLatin1String("operator%"), QLatin1String("__mod__")}, - {QLatin1String("operator%="), QLatin1String("__imod__")}, - {QLatin1String("operator<<"), QLatin1String("__lshift__")}, - {QLatin1String("operator<<="), QLatin1String("__ilshift__")}, - {QLatin1String("operator>>"), QLatin1String("__rshift__")}, - {QLatin1String("operator>>="), QLatin1String("__irshift__")}, - {QLatin1String("operator&"), QLatin1String("__and__")}, - {QLatin1String("operator&="), QLatin1String("__iand__")}, - {QLatin1String("operator|"), QLatin1String("__or__")}, - {QLatin1String("operator|="), QLatin1String("__ior__")}, - {QLatin1String("operator^"), QLatin1String("__xor__")}, - {QLatin1String("operator^="), QLatin1String("__ixor__")}, - {QLatin1String("operator=="), QLatin1String("__eq__")}, - {QLatin1String("operator!="), QLatin1String("__ne__")}, - {QLatin1String("operator<"), QLatin1String("__lt__")}, - {QLatin1String("operator<="), QLatin1String("__le__")}, - {QLatin1String("operator>"), QLatin1String("__gt__")}, - {QLatin1String("operator>="), QLatin1String("__ge__")}, - }; - return result; + return {}; } -static QString getFuncName(const AbstractMetaFunctionCPtr& cppFunc) +struct docRef { - const auto it = operatorMapping().constFind(cppFunc->name()); - QString result = it != operatorMapping().cend() ? it.value() : cppFunc->name(); - result.replace(QLatin1String("::"), QLatin1String(".")); + explicit docRef(const char *kind, QAnyStringView name) : + m_kind(kind), m_name(name) {} + + const char *m_kind; + QAnyStringView m_name; +}; + +static TextStream &operator<<(TextStream &s, const docRef &dr) +{ + s << ':' << dr.m_kind << ":`" << dr.m_name << '`'; + return s; +} + +static QString fileNameToTocEntry(const QString &fileName) +{ + constexpr auto rstSuffix = ".rst"_L1; + + QString result = fileName; + if (result.endsWith(rstSuffix)) + result.chop(rstSuffix.size()); // Remove the .rst extension + // skip namespace if necessary + auto lastDot = result.lastIndexOf(u'.'); + if (lastDot != -1) + result.remove(0, lastDot + 1); return result; } +static void readExtraDoc(const QFileInfo &fi, + const QString &moduleName, + const QString &outputDir, + DocPackage *docPackage, QStringList *extraTocEntries) +{ + // Strip to "Property.rst" in output directory + const QString newFileName = fi.fileName().mid(moduleName.size() + 1); + QFile sourceFile(fi.absoluteFilePath()); + if (!sourceFile.open(QIODevice::ReadOnly|QIODevice::Text)) { + qCWarning(lcShibokenDoc, "%s", qPrintable(msgCannotOpenForReading(sourceFile))); + return; + } + const QByteArray contents = sourceFile.readAll(); + sourceFile.close(); + QFile targetFile(outputDir + u'/' + newFileName); + if (!targetFile.open(QIODevice::WriteOnly|QIODevice::Text)) { + qCWarning(lcShibokenDoc, "%s", qPrintable(msgCannotOpenForWriting(targetFile))); + return; + } + targetFile.write(contents); + if (contents.contains("decorator::")) + docPackage->decoratorPages.append(newFileName); + else + docPackage->classPages.append(newFileName); + extraTocEntries->append(fileNameToTocEntry(newFileName)); +} + +// Format a short documentation reference (automatically dropping the prefix +// by using '~'), usable for property/attributes ("attr"). +struct shortDocRef +{ + explicit shortDocRef(const char *kind, QAnyStringView name) : + m_kind(kind), m_name(name) {} + + const char *m_kind; + QAnyStringView m_name; +}; + +static TextStream &operator<<(TextStream &s, const shortDocRef &sdr) +{ + s << ':' << sdr.m_kind << ":`~" << sdr.m_name << '`'; + return s; +} + +struct functionRef : public docRef +{ + explicit functionRef(QAnyStringView name) : docRef("meth", name) {} +}; + +struct classRef : public shortDocRef +{ + explicit classRef(QAnyStringView name) : shortDocRef("class", name) {} +}; + +struct propRef : public shortDocRef // Attribute/property (short) reference +{ + explicit propRef(const QString &target) : + shortDocRef("attr", target) {} +}; + +struct headline +{ + explicit headline(QAnyStringView title, char underLineChar = '-') : + m_title(title), m_underLineChar(underLineChar) {} + + QAnyStringView m_title; + char m_underLineChar; +}; + +static TextStream &operator<<(TextStream &s, const headline &h) +{ + s << h.m_title << '\n' << Pad(h.m_underLineChar, h.m_title.size()) << "\n\n"; + return s; +} + +struct pyClass +{ + explicit pyClass(QAnyStringView name) : m_name(name) {} + + QAnyStringView m_name; +}; + +static TextStream &operator<<(TextStream &s, pyClass c) +{ + s << ".. py:class:: " << c.m_name << "\n\n"; + return s; +} + +struct currentModule +{ + explicit currentModule(QAnyStringView module) : m_module(module) {} + + QAnyStringView m_module; +}; + +static TextStream &operator<<(TextStream &s, const currentModule &m) +{ + s << ".. currentmodule:: " << m.m_module << "\n\n\n"; + return s; +} + +DocGeneratorOptions QtDocGenerator::m_options; + QtDocGenerator::QtDocGenerator() { - m_parameters.snippetComparison = + m_options.parameters.snippetComparison = ReportHandler::debugLevel() >= ReportHandler::FullDebug; } QtDocGenerator::~QtDocGenerator() = default; -QString QtDocGenerator::fileNameSuffix() const +QString QtDocGenerator::fileNameSuffix() { - return QLatin1String(".rst"); + return u".rst"_s; } -bool QtDocGenerator::shouldGenerate(const AbstractMetaClass *cls) const +bool QtDocGenerator::shouldGenerate(const TypeEntryCPtr &te) const { - return Generator::shouldGenerate(cls) - && cls->typeEntry()->type() != TypeEntry::SmartPointerType; + return Generator::shouldGenerate(te) + && te->type() != TypeEntry::SmartPointerType; } QString QtDocGenerator::fileNameForContext(const GeneratorContext &context) const { - const AbstractMetaClass *metaClass = context.metaClass(); - if (!context.forSmartPointer()) { - return metaClass->name() + fileNameSuffix(); - } - const AbstractMetaType &smartPointerType = context.preciseType(); - QString fileNameBase = getFileNameBaseForSmartPointer(smartPointerType, metaClass); - return fileNameBase + fileNameSuffix(); + return fileNameForContextHelper(context, fileNameSuffix(), + FileNameFlag::UnqualifiedName + | FileNameFlag::KeepCase); } void QtDocGenerator::writeFormattedBriefText(TextStream &s, const Documentation &doc, - const AbstractMetaClass *metaclass) const + const QString &scope) const { - writeFormattedText(s, doc.brief(), doc.format(), metaclass); + writeFormattedText(s, doc.brief(), doc.format(), scope); } void QtDocGenerator::writeFormattedDetailedText(TextStream &s, const Documentation &doc, - const AbstractMetaClass *metaclass) const + const QString &scope) const { - writeFormattedText(s, doc.detailed(), doc.format(), metaclass); + writeFormattedText(s, doc.detailed(), doc.format(), scope); } void QtDocGenerator::writeFormattedText(TextStream &s, const QString &doc, Documentation::Format format, - const AbstractMetaClass *metaClass) const + const QString &scope) const { - QString metaClassName; - - if (metaClass) - metaClassName = metaClass->fullName(); - if (format == Documentation::Native) { - QtXmlToSphinx x(this, m_parameters, doc, metaClassName); + QtXmlToSphinx x(this, m_options.parameters, doc, scope); s << x; } else { - const auto lines = QStringView{doc}.split(QLatin1Char('\n')); + const auto lines = QStringView{doc}.split(u'\n'); int typesystemIndentation = std::numeric_limits<int>::max(); // check how many spaces must be removed from the beginning of each line for (const auto &line : lines) { @@ -225,50 +365,71 @@ void QtDocGenerator::writeFormattedText(TextStream &s, const QString &doc, s << '\n'; } -static void writeInheritedByList(TextStream& s, const AbstractMetaClass* metaClass, +static void writeInheritanceList(TextStream &s, const AbstractMetaClassCList& classes, + const char *label) +{ + s << "**" << label << ":** "; + for (qsizetype i = 0, size = classes.size(); i < size; ++i) { + if (i > 0) + s << ", "; + s << classRef(classes.at(i)->fullName()); + } + s << "\n\n"; +} + +static void writeInheritedByList(TextStream &s, const AbstractMetaClassCPtr &metaClass, const AbstractMetaClassCList& allClasses) { AbstractMetaClassCList res; - for (auto c : allClasses) { - if (c != metaClass && c->inheritsFrom(metaClass)) + for (const auto &c : allClasses) { + if (c != metaClass && inheritsFrom(c, metaClass)) res << c; } - if (res.isEmpty()) - return; + if (!res.isEmpty()) + writeInheritanceList(s, res, "Inherited by"); +} + +static void writeInheritedFromList(TextStream &s, const AbstractMetaClassCPtr &metaClass) +{ + AbstractMetaClassCList res; - s << "**Inherited by:** "; - QStringList classes; - for (auto c : qAsConst(res)) - classes << QLatin1String(":ref:`") + c->name() + QLatin1Char('`'); - s << classes.join(QLatin1String(", ")) << "\n\n"; + recurseClassHierarchy(metaClass, [&res, metaClass](const AbstractMetaClassCPtr &c) { + if (c.get() != metaClass.get()) + res.append(c); + return false; + }); + + if (!res.isEmpty()) + writeInheritanceList(s, res, "Inherits from"); } void QtDocGenerator::generateClass(TextStream &s, const GeneratorContext &classContext) { - const AbstractMetaClass *metaClass = classContext.metaClass(); + AbstractMetaClassCPtr metaClass = classContext.metaClass(); qCDebug(lcShibokenDoc).noquote().nospace() << "Generating Documentation for " << metaClass->fullName(); - m_packages[metaClass->package()] << fileNameForContext(classContext); + m_packages[metaClass->package()].classPages << fileNameForContext(classContext); m_docParser->setPackageName(metaClass->package()); - m_docParser->fillDocumentation(const_cast<AbstractMetaClass*>(metaClass)); - - QString className = metaClass->name(); - s << ".. _" << className << ":" << "\n\n"; - s << ".. currentmodule:: " << metaClass->package() << "\n\n\n"; + m_docParser->fillDocumentation(std::const_pointer_cast<AbstractMetaClass>(metaClass)); - s << className << '\n'; - s << Pad('*', className.count()) << "\n\n"; + s << currentModule(metaClass->package()) << pyClass(metaClass->name()); + Indentation indent(s); auto documentation = metaClass->documentation(); + const QString scope = classScope(metaClass); if (documentation.hasBrief()) - writeFormattedBriefText(s, documentation, metaClass); - - s << ".. inheritance-diagram:: " << metaClass->fullName()<< '\n' - << " :parts: 2\n\n"; - // TODO: This would be a parameter in the future... + writeFormattedBriefText(s, documentation, scope); + if (!metaClass->baseClasses().isEmpty()) { + if (m_options.inheritanceDiagram) { + s << ".. inheritance-diagram:: " << metaClass->fullName()<< '\n' + << " :parts: 2\n\n"; + } else { + writeInheritedFromList(s, metaClass); + } + } writeInheritedByList(s, metaClass, api().classes()); @@ -278,123 +439,113 @@ void QtDocGenerator::generateClass(TextStream &s, const GeneratorContext &classC if (metaClass->attributes().testFlag(AbstractMetaClass::Deprecated)) s << rstDeprecationNote("class"); - writeFunctionList(s, metaClass); + const GeneratorDocumentation doc = generatorDocumentation(metaClass); - //Function list - auto functionList = metaClass->functions(); - std::sort(functionList.begin(), functionList.end(), functionSort); + if (!doc.allFunctions.isEmpty() || !doc.properties.isEmpty()) { + s << '\n' << headline("Synopsis"); + writePropertyToc(s, doc); + writeFunctionToc(s, u"Methods"_s, doc.tocNormalFunctions); + writeFunctionToc(s, u"Virtual methods"_s, doc.tocVirtuals); + writeFunctionToc(s, u"Slots"_s, doc.tocSlotFunctions); + writeFunctionToc(s, u"Signals"_s, doc.tocSignalFunctions); + writeFunctionToc(s, u"Static functions"_s, doc.tocStaticFunctions); + } - s << "\nDetailed Description\n" - "--------------------\n\n" - << ".. _More:\n"; + s << "\n.. note::\n" + " This documentation may contain snippets that were automatically\n" + " translated from C++ to Python. We always welcome contributions\n" + " to the snippet translation. If you see an issue with the\n" + " translation, you can also let us know by creating a ticket on\n" + " https:/bugreports.qt.io/projects/PYSIDE\n\n"; - writeInjectDocumentation(s, TypeSystem::DocModificationPrepend, metaClass, nullptr); - if (!writeInjectDocumentation(s, TypeSystem::DocModificationReplace, metaClass, nullptr)) - writeFormattedDetailedText(s, documentation, metaClass); + s << '\n' << headline("Detailed Description") << ".. _More:\n"; - if (!metaClass->isNamespace()) - writeConstructors(s, metaClass); - writeEnums(s, metaClass); - if (!metaClass->isNamespace()) - writeFields(s, metaClass); + writeInjectDocumentation(s, TypeSystem::DocModificationPrepend, metaClass); + if (!writeInjectDocumentation(s, TypeSystem::DocModificationReplace, metaClass)) + writeFormattedDetailedText(s, documentation, scope); + writeInjectDocumentation(s, TypeSystem::DocModificationAppend, metaClass); + writeEnums(s, metaClass->enums(), scope); - QStringList uniqueFunctions; - for (const auto &func : qAsConst(functionList)) { - if (shouldSkip(func)) - continue; + if (!doc.properties.isEmpty()) + writeProperties(s, doc, metaClass); - if (func->isStatic()) - s << ".. staticmethod:: "; - else - s << ".. method:: "; - - writeFunction(s, metaClass, func, !uniqueFunctions.contains(func->name())); - uniqueFunctions.append(func->name()); - } + if (!metaClass->isNamespace()) + writeFields(s, metaClass); - writeInjectDocumentation(s, TypeSystem::DocModificationAppend, metaClass, nullptr); + writeFunctions(s, doc.allFunctions, metaClass, scope); } -void QtDocGenerator::writeFunctionList(TextStream& s, const AbstractMetaClass* cppClass) +void QtDocGenerator::writeFunctionToc(TextStream &s, const QString &title, + const AbstractMetaFunctionCList &functions) { - QStringList functionList; - QStringList virtualList; - QStringList signalList; - QStringList slotList; - QStringList staticFunctionList; - - const auto &classFunctions = cppClass->functions(); - for (const auto &func : classFunctions) { - if (shouldSkip(func)) - continue; - - QString className; - if (!func->isConstructor()) - className = cppClass->fullName() + QLatin1Char('.'); - else if (func->implementingClass() && func->implementingClass()->enclosingClass()) - className = func->implementingClass()->enclosingClass()->fullName() + QLatin1Char('.'); - QString funcName = getFuncName(func); - - QString str = QLatin1String("def :meth:`"); - - str += funcName; - str += QLatin1Char('<'); - if (!funcName.startsWith(className)) - str += className; - str += funcName; - str += QLatin1String(">` ("); - str += parseArgDocStyle(cppClass, func); - str += QLatin1Char(')'); - - if (func->isStatic()) - staticFunctionList << str; - else if (func->isVirtual()) - virtualList << str; - else if (func->isSignal()) - signalList << str; - else if (func->isSlot()) - slotList << str; - else - functionList << str; + if (!functions.isEmpty()) { + s << headline(title, '^') + << ".. container:: function_list\n\n" << indent; + // Functions are sorted by the Metabuilder; erase overloads + QStringList toc; + toc.reserve(functions.size()); + std::transform(functions.cbegin(), functions.end(), + std::back_inserter(toc), getFuncName); + toc.erase(std::unique(toc.begin(), toc.end()), toc.end()); + for (const auto &func : toc) + s << "* def " << functionRef(func) << '\n'; + s << outdent << "\n\n"; } +} - if (!functionList.isEmpty() || !staticFunctionList.isEmpty()) { - QtXmlToSphinx::Table functionTable; - - s << "\nSynopsis\n--------\n\n"; +void QtDocGenerator::writePropertyToc(TextStream &s, + const GeneratorDocumentation &doc) +{ + if (doc.properties.isEmpty()) + return; - writeFunctionBlock(s, QLatin1String("Functions"), functionList); - writeFunctionBlock(s, QLatin1String("Virtual functions"), virtualList); - writeFunctionBlock(s, QLatin1String("Slots"), slotList); - writeFunctionBlock(s, QLatin1String("Signals"), signalList); - writeFunctionBlock(s, QLatin1String("Static functions"), staticFunctionList); + s << headline("Properties", '^') + << ".. container:: function_list\n\n" << indent; + for (const auto &prop : doc.properties) { + s << "* " << propRef(propertyRefTarget(prop.name)); + if (prop.documentation.hasBrief()) + s << " - " << prop.documentation.brief(); + s << '\n'; } + s << outdent << "\n\n"; } -void QtDocGenerator::writeFunctionBlock(TextStream& s, const QString& title, QStringList& functions) +void QtDocGenerator::writeProperties(TextStream &s, + const GeneratorDocumentation &doc, + const AbstractMetaClassCPtr &cppClass) const { - if (!functions.isEmpty()) { - s << title << '\n' - << Pad('^', title.size()) << '\n'; - - std::sort(functions.begin(), functions.end()); - - s << ".. container:: function_list\n\n"; - Indentation indentation(s); - for (const QString &func : qAsConst(functions)) - s << "* " << func << '\n'; - s << "\n\n"; + s << "\n.. note:: Properties can be used directly when " + << "``from __feature__ import true_property`` is used or via accessor " + << "functions otherwise.\n\n"; + + const QString scope = classScope(cppClass); + for (const auto &prop : doc.properties) { + const QString type = translateToPythonType(prop.type, cppClass, /* createRef */ false); + s << ".. py:property:: " << propertyRefTarget(prop.name) + << "\n :type: " << type << "\n\n\n"; + if (!prop.documentation.isEmpty()) + writeFormattedText(s, prop.documentation.detailed(), Documentation::Native, scope); + s << "**Access functions:**\n"; + if (prop.getter) + s << " * " << functionRef(prop.getter->name()) << '\n'; + if (prop.setter) + s << " * " << functionRef(prop.setter->name()) << '\n'; + if (prop.reset) + s << " * " << functionRef(prop.reset->name()) << '\n'; + if (prop.notify) + s << " * Signal " << functionRef(prop.notify->name()) << '\n'; + s << '\n'; } } -void QtDocGenerator::writeEnums(TextStream& s, const AbstractMetaClass* cppClass) const +void QtDocGenerator::writeEnums(TextStream &s, const AbstractMetaEnumList &enums, + const QString &scope) const { - static const QString section_title = QLatin1String(".. attribute:: "); - - for (const AbstractMetaEnum &en : cppClass->enums()) { - s << section_title << cppClass->fullName() << '.' << en.name() << "\n\n"; - writeFormattedDetailedText(s, en.documentation(), cppClass); + for (const AbstractMetaEnum &en : enums) { + s << pyClass(en.name()); + Indentation indent(s); + writeFormattedDetailedText(s, en.documentation(), scope); const auto version = versionOf(en.typeEntry()); if (!version.isNull()) s << rstVersionAdded(version); @@ -402,116 +553,61 @@ void QtDocGenerator::writeEnums(TextStream& s, const AbstractMetaClass* cppClass } -void QtDocGenerator::writeFields(TextStream& s, const AbstractMetaClass* cppClass) const +void QtDocGenerator::writeFields(TextStream &s, const AbstractMetaClassCPtr &cppClass) const { - static const QString section_title = QLatin1String(".. attribute:: "); + constexpr auto section_title = ".. attribute:: "_L1; + const QString scope = classScope(cppClass); for (const AbstractMetaField &field : cppClass->fields()) { s << section_title << cppClass->fullName() << "." << field.name() << "\n\n"; - writeFormattedDetailedText(s, field.documentation(), cppClass); + writeFormattedDetailedText(s, field.documentation(), scope); } } -void QtDocGenerator::writeConstructors(TextStream& s, const AbstractMetaClass* cppClass) const +QString QtDocGenerator::formatArgs(const AbstractMetaFunctionCPtr &func) { - static const QString sectionTitle = QLatin1String(".. class:: "); - - auto lst = cppClass->queryFunctions(FunctionQueryOption::Constructors | FunctionQueryOption::Visible); - for (int i = lst.size() - 1; i >= 0; --i) { - if (lst.at(i)->isModifiedRemoved() || lst.at(i)->functionType() == AbstractMetaFunction::MoveConstructorFunction) - lst.removeAt(i); - } - - bool first = true; - QHash<QString, AbstractMetaArgument> arg_map; - - if (lst.isEmpty()) { - s << sectionTitle << cppClass->fullName(); - } else { - QByteArray pad; - for (const auto &func : qAsConst(lst)) { - s << pad; - if (first) { - first = false; - s << sectionTitle; - pad = QByteArray(sectionTitle.size(), ' '); - } - s << functionSignature(cppClass, func) << "\n\n"; - - const auto version = versionOf(func->typeEntry()); - if (!version.isNull()) - s << pad << rstVersionAdded(version); - if (func->attributes().testFlag(AbstractMetaFunction::Deprecated)) - s << pad << rstDeprecationNote("constructor"); - - const AbstractMetaArgumentList &arguments = func->arguments(); - for (const AbstractMetaArgument &arg : arguments) { - if (!arg_map.contains(arg.name())) { - arg_map.insert(arg.name(), arg); - } - } - } - } - - s << '\n'; - - for (auto it = arg_map.cbegin(), end = arg_map.cend(); it != end; ++it) { - s.indent(2); - writeParameterType(s, cppClass, it.value()); - s.outdent(2); - } - - s << '\n'; - - for (const auto &func : qAsConst(lst)) - writeFormattedDetailedText(s, func->documentation(), cppClass); -} - -QString QtDocGenerator::parseArgDocStyle(const AbstractMetaClass* /* cppClass */, - const AbstractMetaFunctionCPtr &func) -{ - QString ret; + QString ret = u"("_s; int optArgs = 0; const AbstractMetaArgumentList &arguments = func->arguments(); for (const AbstractMetaArgument &arg : arguments) { - if (func->argumentRemoved(arg.argumentIndex() + 1)) + if (arg.isModifiedRemoved()) continue; bool thisIsoptional = !arg.defaultValueExpression().isEmpty(); if (optArgs || thisIsoptional) { - ret += QLatin1Char('['); + ret += u'['; optArgs++; } if (arg.argumentIndex() > 0) - ret += QLatin1String(", "); + ret += u", "_s; ret += arg.name(); if (thisIsoptional) { QString defValue = arg.defaultValueExpression(); - if (defValue == QLatin1String("QString()")) { - defValue = QLatin1String("\"\""); - } else if (defValue == QLatin1String("QStringList()") - || defValue.startsWith(QLatin1String("QVector")) - || defValue.startsWith(QLatin1String("QList"))) { - defValue = QLatin1String("list()"); - } else if (defValue == QLatin1String("QVariant()")) { - defValue = none(); + if (defValue == u"QString()") { + defValue = u"\"\""_s; + } else if (defValue == u"QStringList()" + || defValue.startsWith(u"QVector") + || defValue.startsWith(u"QList")) { + defValue = u"list()"_s; + } else if (defValue == u"QVariant()") { + defValue = none; } else { - defValue.replace(QLatin1String("::"), QLatin1String(".")); - if (defValue == QLatin1String("nullptr")) - defValue = none(); - else if (defValue == QLatin1String("0") && arg.type().isObject()) - defValue = none(); + defValue.replace(u"::"_s, u"."_s); + if (defValue == u"nullptr") + defValue = none; + else if (defValue == u"0" && arg.type().isObject()) + defValue = none; } - ret += QLatin1Char('=') + defValue; + ret += u'=' + defValue; } } - ret += QString(optArgs, QLatin1Char(']')); + ret += QString(optArgs, u']') + u')'; return ret; } @@ -521,11 +617,9 @@ void QtDocGenerator::writeDocSnips(TextStream &s, TypeSystem::Language language) { Indentation indentation(s); - QStringList invalidStrings; - const static QString startMarkup = QLatin1String("[sphinx-begin]"); - const static QString endMarkup = QLatin1String("[sphinx-end]"); - - invalidStrings << QLatin1String("*") << QLatin1String("//") << QLatin1String("/*") << QLatin1String("*/"); + static const QStringList invalidStrings{u"*"_s, u"//"_s, u"/*"_s, u"*/"_s}; + const static QString startMarkup = u"[sphinx-begin]"_s; + const static QString endMarkup = u"[sphinx-end]"_s; for (const CodeSnip &snip : codeSnips) { if ((snip.position != position) || @@ -534,19 +628,19 @@ void QtDocGenerator::writeDocSnips(TextStream &s, QString code = snip.code(); while (code.contains(startMarkup) && code.contains(endMarkup)) { - int startBlock = code.indexOf(startMarkup) + startMarkup.size(); - int endBlock = code.indexOf(endMarkup); + const auto startBlock = code.indexOf(startMarkup) + startMarkup.size(); + const auto endBlock = code.indexOf(endMarkup); if ((startBlock == -1) || (endBlock == -1)) break; QString codeBlock = code.mid(startBlock, endBlock - startBlock); - const QStringList rows = codeBlock.split(QLatin1Char('\n')); + const QStringList rows = codeBlock.split(u'\n'); int currentRow = 0; - int offset = 0; + qsizetype offset = 0; for (QString row : rows) { - for (const QString &invalidString : qAsConst(invalidStrings)) + for (const QString &invalidString : std::as_const(invalidStrings)) row.remove(invalidString); if (row.trimmed().size() == 0) { @@ -558,9 +652,9 @@ void QtDocGenerator::writeDocSnips(TextStream &s, if (currentRow == 0) { //find offset for (auto c : row) { - if (c == QLatin1Char(' ')) + if (c == u' ') offset++; - else if (c == QLatin1Char('\n')) + else if (c == u'\n') offset = 0; else break; @@ -575,304 +669,470 @@ void QtDocGenerator::writeDocSnips(TextStream &s, } } -bool QtDocGenerator::writeInjectDocumentation(TextStream& s, - TypeSystem::DocModificationMode mode, - const AbstractMetaClass* cppClass, - const AbstractMetaFunctionCPtr &func) +bool QtDocGenerator::writeDocModifications(TextStream &s, + const DocModificationList &mods, + TypeSystem::DocModificationMode mode, + const QString &scope) const { - Indentation indentation(s); bool didSomething = false; - - const DocModificationList &mods = cppClass->typeEntry()->docModifications(); for (const DocModification &mod : mods) { if (mod.mode() == mode) { - bool modOk = func ? mod.signature() == func->minimalSignature() : mod.signature().isEmpty(); - - if (modOk) { - Documentation::Format fmt; - - if (mod.format() == TypeSystem::NativeCode) - fmt = Documentation::Native; - else if (mod.format() == TypeSystem::TargetLangCode) - fmt = Documentation::Target; - else - continue; - - writeFormattedText(s, mod.code(), fmt, cppClass); + switch (mod.format()) { + case TypeSystem::NativeCode: + writeFormattedText(s, mod.code(), Documentation::Native, scope); + didSomething = true; + break; + case TypeSystem::TargetLangCode: + writeFormattedText(s, mod.code(), Documentation::Target, scope); didSomething = true; + break; + default: + break; } } } + return didSomething; +} +bool QtDocGenerator::writeInjectDocumentation(TextStream &s, + TypeSystem::DocModificationMode mode, + const AbstractMetaClassCPtr &cppClass) const +{ + const bool didSomething = + writeDocModifications(s, DocParser::getDocModifications(cppClass), + mode, classScope(cppClass)); s << '\n'; - // TODO: Deprecate the use of doc string on glue code. + // FIXME PYSIDE-7: Deprecate the use of doc string on glue code. // This is pre "add-function" and "inject-documentation" tags. const TypeSystem::CodeSnipPosition pos = mode == TypeSystem::DocModificationPrepend ? TypeSystem::CodeSnipPositionBeginning : TypeSystem::CodeSnipPositionEnd; - if (func) - writeDocSnips(s, func->injectedCodeSnips(), pos, TypeSystem::TargetLangCode); - else - writeDocSnips(s, cppClass->typeEntry()->codeSnips(), pos, TypeSystem::TargetLangCode); + writeDocSnips(s, cppClass->typeEntry()->codeSnips(), pos, TypeSystem::TargetLangCode); return didSomething; } -QString QtDocGenerator::functionSignature(const AbstractMetaClass* cppClass, - const AbstractMetaFunctionCPtr &func) +bool QtDocGenerator::writeInjectDocumentation(TextStream &s, + TypeSystem::DocModificationMode mode, + const DocModificationList &modifications, + const AbstractMetaFunctionCPtr &func, + const QString &scope) const { - QString funcName; + const bool didSomething = writeDocModifications(s, modifications, mode, scope); + s << '\n'; - funcName = cppClass->fullName(); - if (!func->isConstructor()) - funcName += QLatin1Char('.') + getFuncName(func); + // FIXME PYSIDE-7: Deprecate the use of doc string on glue code. + // This is pre "add-function" and "inject-documentation" tags. + const TypeSystem::CodeSnipPosition pos = mode == TypeSystem::DocModificationPrepend + ? TypeSystem::CodeSnipPositionBeginning : TypeSystem::CodeSnipPositionEnd; + writeDocSnips(s, func->injectedCodeSnips(), pos, TypeSystem::TargetLangCode); + return didSomething; +} - return funcName + QLatin1Char('(') + parseArgDocStyle(cppClass, func) - + QLatin1Char(')'); +static QString inline toRef(const QString &t) +{ + return ":class:`~"_L1 + t + u'`'; } QString QtDocGenerator::translateToPythonType(const AbstractMetaType &type, - const AbstractMetaClass* cppClass) const + const AbstractMetaClassCPtr &cppClass, + bool createRef) const { static const QStringList nativeTypes = - {boolT(), floatT(), intT(), pyObjectT(), pyStrT()}; + {boolT, floatT, intT, pyObjectT, pyStrT}; - const QString name = type.name(); + QString name = type.name(); if (nativeTypes.contains(name)) return name; - static const QMap<QString, QString> typeMap = { - { cPyObjectT(), pyObjectT() }, - { qStringT(), pyStrT() }, - { QLatin1String("uchar"), pyStrT() }, - { QLatin1String("QStringList"), QLatin1String("list of strings") }, - { qVariantT(), pyObjectT() }, - { QLatin1String("quint32"), intT() }, - { QLatin1String("uint32_t"), intT() }, - { QLatin1String("quint64"), intT() }, - { QLatin1String("qint64"), intT() }, - { QLatin1String("size_t"), intT() }, - { QLatin1String("int64_t"), intT() }, - { QLatin1String("qreal"), floatT() } + if (type.typeUsagePattern() == AbstractMetaType::PrimitivePattern) { + const auto &basicName = basicReferencedTypeEntry(type.typeEntry())->name(); + if (AbstractMetaType::cppSignedIntTypes().contains(basicName) + || AbstractMetaType::cppUnsignedIntTypes().contains(basicName)) { + return intT; + } + if (AbstractMetaType::cppFloatTypes().contains(basicName)) + return floatT; + } + + static const QSet<QString> stringTypes = { + u"uchar"_s, u"std::string"_s, u"std::wstring"_s, + u"std::stringview"_s, u"std::wstringview"_s, + qStringT, u"QStringView"_s, u"QAnyStringView"_s, u"QUtf8StringView"_s + }; + if (stringTypes.contains(name)) + return pyStrT; + + static const QHash<QString, QString> typeMap = { + { cPyObjectT, pyObjectT }, + { u"QStringList"_s, u"list of strings"_s }, + { qVariantT, pyObjectT } }; - const auto found = typeMap.find(name); - if (found != typeMap.end()) + const auto found = typeMap.constFind(name); + if (found != typeMap.cend()) return found.value(); - QString strType; - if (type.isConstant() && name == QLatin1String("char") && type.indirections() == 1) { - strType = QLatin1String("str"); - } else if (name.startsWith(unsignedShortT())) { - strType = intT(); - } else if (name.startsWith(unsignedT())) { // uint and ulong - strType = intT(); - } else if (type.isContainer()) { + if (type.isFlags()) { + const auto fte = std::static_pointer_cast<const FlagsTypeEntry>(type.typeEntry()); + auto enumTypeEntry = fte->originator(); + auto enumName = enumTypeEntry->targetLangName(); + if (createRef) + enumName.prepend(enumTypeEntry->targetLangPackage() + u'.'); + return "Combination of "_L1 + (createRef ? toRef(enumName) : enumName); + } else if (type.isEnum()) { + auto enumTypeEntry = std::static_pointer_cast<const EnumTypeEntry>(type.typeEntry()); + auto enumName = enumTypeEntry->targetLangName(); + if (createRef) + enumName.prepend(enumTypeEntry->targetLangPackage() + u'.'); + return createRef ? toRef(enumName) : enumName; + } + + if (type.isConstant() && name == "char"_L1 && type.indirections() == 1) + return "str"_L1; + + if (type.isContainer()) { QString strType = translateType(type, cppClass, Options(ExcludeConst) | ExcludeReference); - strType.remove(QLatin1Char('*')); - strType.remove(QLatin1Char('>')); - strType.remove(QLatin1Char('<')); - strType.replace(QLatin1String("::"), QLatin1String(".")); - if (strType.contains(QLatin1String("QList")) || strType.contains(QLatin1String("QVector"))) { - strType.replace(QLatin1String("QList"), QLatin1String("list of ")); - strType.replace(QLatin1String("QVector"), QLatin1String("list of ")); - } else if (strType.contains(QLatin1String("QHash")) || strType.contains(QLatin1String("QMap"))) { - strType.remove(QLatin1String("QHash")); - strType.remove(QLatin1String("QMap")); - QStringList types = strType.split(QLatin1Char(',')); + strType.remove(u'*'); + strType.remove(u'>'); + strType.remove(u'<'); + strType.replace(u"::"_s, u"."_s); + if (strType.contains(u"QList") || strType.contains(u"QVector")) { + strType.replace(u"QList"_s, u"list of "_s); + strType.replace(u"QVector"_s, u"list of "_s); + } else if (strType.contains(u"QHash") || strType.contains(u"QMap")) { + strType.remove(u"QHash"_s); + strType.remove(u"QMap"_s); + QStringList types = strType.split(u','); strType = QString::fromLatin1("Dictionary with keys of type %1 and values of type %2.") .arg(types[0], types[1]); } - } else { - auto k = AbstractMetaClass::findClass(api().classes(), type.typeEntry()); - strType = k ? k->fullName() : type.name(); - strType = QStringLiteral(":any:`") + strType + QLatin1Char('`'); + return strType; } - return strType; + + if (auto k = AbstractMetaClass::findClass(api().classes(), type.typeEntry())) + return createRef ? toRef(k->fullName()) : k->name(); + + return createRef ? toRef(name) : name; } -void QtDocGenerator::writeParameterType(TextStream& s, const AbstractMetaClass* cppClass, +QString QtDocGenerator::getFuncName(const AbstractMetaFunctionCPtr &cppFunc) +{ + if (cppFunc->isConstructor()) + return "__init__"_L1; + QString result = cppFunc->name(); + if (cppFunc->isOperatorOverload()) { + const QString pythonOperator = Generator::pythonOperatorFunctionName(result); + if (!pythonOperator.isEmpty()) + return pythonOperator; + } + result.replace(u"::"_s, u"."_s); + return result; +} + +void QtDocGenerator::writeParameterType(TextStream &s, + const AbstractMetaClassCPtr &cppClass, const AbstractMetaArgument &arg) const { s << ":param " << arg.name() << ": " << translateToPythonType(arg.type(), cppClass) << '\n'; } -void QtDocGenerator::writeFunctionParametersType(TextStream &s, const AbstractMetaClass *cppClass, +void QtDocGenerator::writeFunctionParametersType(TextStream &s, + const AbstractMetaClassCPtr &cppClass, const AbstractMetaFunctionCPtr &func) const { s << '\n'; const AbstractMetaArgumentList &funcArgs = func->arguments(); for (const AbstractMetaArgument &arg : funcArgs) { - - if (func->argumentRemoved(arg.argumentIndex() + 1)) - continue; - - writeParameterType(s, cppClass, arg); + if (!arg.isModifiedRemoved()) + writeParameterType(s, cppClass, arg); } - if (!func->isConstructor() && !func->isVoid()) { - - QString retType; + QString retType; + if (!func->isConstructor()) { // check if the return type was modified - for (const auto &mod : func->modifications()) { - for (const ArgumentModification &argMod : mod.argument_mods()) { - if (argMod.index() == 0) { - retType = argMod.modifiedType(); - break; - } - } - } - - if (retType.isEmpty()) + retType = func->modifiedTypeName(); + if (retType.isEmpty() && !func->isVoid()) retType = translateToPythonType(func->type(), cppClass); - s << ":rtype: " << retType << '\n'; } + + if (!retType.isEmpty()) + s << ":rtype: " << retType << '\n'; + s << '\n'; } -void QtDocGenerator::writeFunction(TextStream& s, const AbstractMetaClass* cppClass, - const AbstractMetaFunctionCPtr &func, bool indexed) +static bool containsFunctionDirective(const DocModification &dm) { - s << functionSignature(cppClass, func); + return dm.mode() != TypeSystem::DocModificationXPathReplace + && dm.code().contains(".. py:"_L1); +} - { +void QtDocGenerator::writeFunctions(TextStream &s, const AbstractMetaFunctionCList &funcs, + const AbstractMetaClassCPtr &cppClass, const QString &scope) +{ + QString lastName; + for (const auto &func : funcs) { + const bool indexed = func->name() != lastName; + lastName = func->name(); + writeFunction(s, func, cppClass, scope, indexed); + } +} + +void QtDocGenerator::writeFunction(TextStream &s, const AbstractMetaFunctionCPtr &func, + const AbstractMetaClassCPtr &cppClass, + const QString &scope, bool indexed) +{ + const auto modifications = DocParser::getDocModifications(func, cppClass); + + // Enable injecting parameter documentation by adding a complete function directive. + if (std::none_of(modifications.cbegin(), modifications.cend(), containsFunctionDirective)) { + if (func->ownerClass() == nullptr) + s << ".. py:function:: "; + else + s << (func->isStatic() ? ".. py:staticmethod:: " : ".. py:method:: "); + s << getFuncName(func) << formatArgs(func); Indentation indentation(s); if (!indexed) s << "\n:noindex:"; + if (func->cppAttributes().testFlag(FunctionAttribute::Final)) + s << "\n:final:"; + else if (func->isAbstract()) + s << "\n:abstractmethod:"; s << "\n\n"; writeFunctionParametersType(s, cppClass, func); const auto version = versionOf(func->typeEntry()); if (!version.isNull()) s << rstVersionAdded(version); - if (func->attributes().testFlag(AbstractMetaFunction::Deprecated)) + if (func->isDeprecated()) s << rstDeprecationNote("function"); } - writeInjectDocumentation(s, TypeSystem::DocModificationPrepend, cppClass, func); - if (!writeInjectDocumentation(s, TypeSystem::DocModificationReplace, cppClass, func)) { - writeFormattedBriefText(s, func->documentation(), cppClass); - writeFormattedDetailedText(s, func->documentation(), cppClass); + + writeFunctionDocumentation(s, func, modifications, scope); + + if (auto propIndex = func->propertySpecIndex(); propIndex >= 0) { + const QString name = cppClass->propertySpecs().at(propIndex).name(); + const QString target = propertyRefTarget(name); + if (func->isPropertyReader()) + s << "\nGetter of property " << propRef(target) << " .\n\n"; + else if (func->isPropertyWriter()) + s << "\nSetter of property " << propRef(target) << " .\n\n"; + else if (func->isPropertyResetter()) + s << "\nReset function of property " << propRef(target) << " .\n\n"; + else if (func->attributes().testFlag(AbstractMetaFunction::Attribute::PropertyNotify)) + s << "\nNotification signal of property " << propRef(target) << " .\n\n"; } - writeInjectDocumentation(s, TypeSystem::DocModificationAppend, cppClass, func); } -static void writeFancyToc(TextStream& s, const QStringList& items) +void QtDocGenerator::writeFunctionDocumentation(TextStream &s, const AbstractMetaFunctionCPtr &func, + const DocModificationList &modifications, + const QString &scope) const + +{ + writeInjectDocumentation(s, TypeSystem::DocModificationPrepend, modifications, func, scope); + if (!writeInjectDocumentation(s, TypeSystem::DocModificationReplace, modifications, func, scope)) { + writeFormattedBriefText(s, func->documentation(), scope); + writeFormattedDetailedText(s, func->documentation(), scope); + } + writeInjectDocumentation(s, TypeSystem::DocModificationAppend, modifications, func, scope); +} + +static QStringList fileListToToc(const QStringList &items) +{ + QStringList result; + result.reserve(items.size()); + std::transform(items.cbegin(), items.cend(), std::back_inserter(result), + fileNameToTocEntry); + return result; +} + +static QStringList functionListToToc(const AbstractMetaFunctionCList &functions) +{ + QStringList result; + result.reserve(functions.size()); + for (const auto &f : functions) + result.append(f->name()); + // Functions are sorted by the Metabuilder; erase overloads + result.erase(std::unique(result.begin(), result.end()), result.end()); + return result; +} + +static QStringList enumListToToc(const AbstractMetaEnumList &enums) +{ + QStringList result; + result.reserve(enums.size()); + for (const auto &e : enums) + result.append(e.name()); + return result; +} + +// Sort entries for a TOC by first character, dropping the +// leading common Qt prefixes like 'Q'. +static QChar sortKey(const QString &key) +{ + const auto size = key.size(); + if (size >= 2 && (key.at(0) == u'Q' || key.at(0) == u'q') && key.at(1).isUpper()) + return key.at(1); // "QClass" -> 'C', "qSin()" -> 'S' + if (size >= 3 && key.startsWith("Q_"_L1)) + return key.at(2).toUpper(); // "Q_ARG" -> 'A' + if (size >= 4 && key.startsWith("QT_"_L1)) + return key.at(3).toUpper(); // "QT_TR" -> 'T' + auto idx = 0; + for (; idx < size && key.at(idx) == u'_'; ++idx) { + } // "__init__" -> 'I' + return idx < size ? key.at(idx).toUpper() : u'A'; +} + +static void writeFancyToc(TextStream& s, QAnyStringView title, + const QStringList& items, + QLatin1StringView referenceType) { using TocMap = QMap<QChar, QStringList>; + + if (items.isEmpty()) + return; + TocMap tocMap; - QChar Q = QLatin1Char('Q'); - QChar idx; - for (QString item : items) { - if (item.isEmpty()) - continue; - item.chop(4); // Remove the .rst extension - // skip namespace if necessary - const QString className = item.split(QLatin1Char('.')).last(); - if (className.startsWith(Q) && className.length() > 1) - idx = className[1]; - else - idx = className[0]; - tocMap[idx] << item; - } + for (const QString &item : items) + tocMap[sortKey(item)] << item; static const qsizetype numColumns = 4; QtXmlToSphinx::Table table; for (auto it = tocMap.cbegin(), end = tocMap.cend(); it != end; ++it) { QtXmlToSphinx::TableRow row; - const QString charEntry = QLatin1String("**") + it.key() + QLatin1String("**"); + const QString charEntry = u"**"_s + it.key() + u"**"_s; row << QtXmlToSphinx::TableCell(charEntry); - for (const QString &item : qAsConst(it.value())) { + for (const QString &item : std::as_const(it.value())) { if (row.size() >= numColumns) { table.appendRow(row); row.clear(); row << QtXmlToSphinx::TableCell(QString{}); } - const QString entry = QLatin1String("* :doc:`") + item + QLatin1Char('`'); + const QString entry = "* :"_L1 + referenceType + ":`"_L1 + item + u'`'; row << QtXmlToSphinx::TableCell(entry); } - if (!row.isEmpty()) + if (row.size() > 1) table.appendRow(row); } table.normalize(); - s << ".. container:: pysidetoc\n\n"; + s << '\n' << headline(title) << ".. container:: pysidetoc\n\n"; table.format(s); } bool QtDocGenerator::finishGeneration() { - if (!api().classes().isEmpty()) + for (const auto &f : api().globalFunctions()) { + auto ncf = std::const_pointer_cast<AbstractMetaFunction>(f); + m_docParser->fillGlobalFunctionDocumentation(ncf); + m_packages[f->targetLangPackage()].globalFunctions.append(f); + } + + for (auto e : api().globalEnums()) { + m_docParser->fillGlobalEnumDocumentation(e); + m_packages[e.typeEntry()->targetLangPackage()].globalEnums.append(e); + } + + if (!m_packages.isEmpty()) writeModuleDocumentation(); - if (!m_additionalDocumentationList.isEmpty()) + if (!m_options.additionalDocumentationList.isEmpty()) writeAdditionalDocumentation(); + if (!m_options.inheritanceFile.isEmpty() && !writeInheritanceFile()) + return false; return true; } +bool QtDocGenerator::writeInheritanceFile() +{ + QFile inheritanceFile(m_options.inheritanceFile); + if (!inheritanceFile.open(QIODevice::WriteOnly | QIODevice::Text)) + throw Exception(msgCannotOpenForWriting(m_options.inheritanceFile)); + + QJsonObject dict; + for (const auto &c : api().classes()) { + const auto &bases = c->baseClasses(); + if (!bases.isEmpty()) { + QJsonArray list; + for (const auto &base : bases) + list.append(QJsonValue(base->fullName())); + dict[c->fullName()] = list; + } + } + QJsonDocument document; + document.setObject(dict); + inheritanceFile.write(document.toJson(QJsonDocument::Compact)); + return true; +} + +// Remove function entries that have extra documentation pages +static inline void removeExtraDocs(const QStringList &extraTocEntries, + AbstractMetaFunctionCList *functions) +{ + auto predicate = [&extraTocEntries](const AbstractMetaFunctionCPtr &f) { + return extraTocEntries.contains(f->name()); + }; + functions->erase(std::remove_if(functions->begin(),functions->end(), predicate), + functions->end()); +} + void QtDocGenerator::writeModuleDocumentation() { - QMap<QString, QStringList>::iterator it = m_packages.begin(); - for (; it != m_packages.end(); ++it) { - std::sort(it.value().begin(), it.value().end()); + for (auto it = m_packages.begin(), end = m_packages.end(); it != end; ++it) { + auto &docPackage = it.value(); + std::sort(docPackage.classPages.begin(), docPackage.classPages.end()); QString key = it.key(); - key.replace(QLatin1Char('.'), QLatin1Char('/')); - QString outputDir = outputDirectory() + QLatin1Char('/') + key; - FileOut output(outputDir + QLatin1String("/index.rst")); + key.replace(u'.', u'/'); + QString outputDir = outputDirectory() + u'/' + key; + FileOut output(outputDir + u"/index.rst"_s); TextStream& s = output.stream; const QString &title = it.key(); - s << ".. module:: " << title << "\n\n" - << title << '\n' - << Pad('*', title.length()) << "\n\n"; + s << ".. module:: " << title << "\n\n" << headline(title, '*'); // Store the it.key() in a QString so that it can be stripped off unwanted // information when neeeded. For example, the RST files in the extras directory // doesn't include the PySide# prefix in their names. QString moduleName = it.key(); - const int lastIndex = moduleName.lastIndexOf(QLatin1Char('.')); + const int lastIndex = moduleName.lastIndexOf(u'.'); if (lastIndex >= 0) moduleName.remove(0, lastIndex + 1); // Search for extra-sections - if (!m_extraSectionDir.isEmpty()) { - QDir extraSectionDir(m_extraSectionDir); + QStringList extraTocEntries; + if (!m_options.extraSectionDir.isEmpty()) { + QDir extraSectionDir(m_options.extraSectionDir); if (!extraSectionDir.exists()) { - const QString m = QStringLiteral("Extra sections directory ") + - m_extraSectionDir + QStringLiteral(" doesn't exist"); + const QString m = u"Extra sections directory "_s + + m_options.extraSectionDir + u" doesn't exist"_s; throw Exception(m); } // Filter for "QtCore.Property.rst", skipping module doc "QtCore.rst" - const QString filter = moduleName + QLatin1String(".?*.rst"); + const QString filter = moduleName + u".?*.rst"_s; const auto fileList = extraSectionDir.entryInfoList({filter}, QDir::Files, QDir::Name); - for (const auto &fi : fileList) { - // Strip to "Property.rst" in output directory - const QString newFileName = fi.fileName().mid(moduleName.size() + 1); - it.value().append(newFileName); - const QString newFilePath = outputDir + QLatin1Char('/') + newFileName; - if (QFile::exists(newFilePath)) - QFile::remove(newFilePath); - if (!QFile::copy(fi.absoluteFilePath(), newFilePath)) { - qCDebug(lcShibokenDoc).noquote().nospace() << "Error copying extra doc " - << QDir::toNativeSeparators(fi.absoluteFilePath()) - << " to " << QDir::toNativeSeparators(newFilePath); - } - } + for (const auto &fi : fileList) + readExtraDoc(fi, moduleName, outputDir, &docPackage, &extraTocEntries); } - writeFancyToc(s, it.value()); + removeExtraDocs(extraTocEntries, &docPackage.globalFunctions); + const bool hasGlobals = !docPackage.globalFunctions.isEmpty() + || !docPackage.globalEnums.isEmpty(); + const QString globalsPage = moduleName + "_globals.rst"_L1; s << ".. container:: hide\n\n" << indent << ".. toctree::\n" << indent << ":maxdepth: 1\n\n"; - for (const QString &className : qAsConst(it.value())) + if (hasGlobals) + s << globalsPage << '\n'; + for (const QString &className : std::as_const(docPackage.classPages)) s << className << '\n'; - s << "\n\n" << outdent << outdent - << "Detailed Description\n--------------------\n\n"; + s << "\n\n" << outdent << outdent << headline("Detailed Description"); // module doc is always wrong and C++istic, so go straight to the extra directory! - QFile moduleDoc(m_extraSectionDir + QLatin1Char('/') + moduleName - + QLatin1String(".rst")); + QFile moduleDoc(m_options.extraSectionDir + u'/' + moduleName + + u".rst"_s); if (moduleDoc.open(QIODevice::ReadOnly | QIODevice::Text)) { s << moduleDoc.readAll(); moduleDoc.close(); @@ -882,14 +1142,48 @@ void QtDocGenerator::writeModuleDocumentation() if (moduleDoc.format() == Documentation::Native) { QString context = it.key(); QtXmlToSphinx::stripPythonQualifiers(&context); - QtXmlToSphinx x(this, m_parameters, moduleDoc.detailed(), context); + QtXmlToSphinx x(this, m_options.parameters, moduleDoc.detailed(), context); s << x; } else { s << moduleDoc.detailed(); } } + + writeFancyToc(s, "List of Classes", fileListToToc(docPackage.classPages), + "class"_L1); + writeFancyToc(s, "List of Decorators", fileListToToc(docPackage.decoratorPages), + "deco"_L1); + writeFancyToc(s, "List of Functions", functionListToToc(docPackage.globalFunctions), + "py:func"_L1); + writeFancyToc(s, "List of Enumerations", enumListToToc(docPackage.globalEnums), + "any"_L1); + output.done(); + + if (hasGlobals) + writeGlobals(it.key(), outputDir + u'/' + globalsPage, docPackage); + } +} + +void QtDocGenerator::writeGlobals(const QString &package, + const QString &fileName, + const DocPackage &docPackage) +{ + FileOut output(fileName); + TextStream &s = output.stream; + + // Write out functions with injected documentation + if (!docPackage.globalFunctions.isEmpty()) { + s << currentModule(package) << headline("Functions"); + writeFunctions(s, docPackage.globalFunctions, {}, {}); + } + + if (!docPackage.globalEnums.isEmpty()) { + s << headline("Enumerations"); + writeEnums(s, docPackage.globalEnums, package); } + + output.done(); } static inline QString msgNonExistentAdditionalDocFile(const QString &dir, @@ -904,7 +1198,7 @@ static inline QString msgNonExistentAdditionalDocFile(const QString &dir, void QtDocGenerator::writeAdditionalDocumentation() const { - QFile additionalDocumentationFile(m_additionalDocumentationList); + QFile additionalDocumentationFile(m_options.additionalDocumentationList); if (!additionalDocumentationFile.open(QIODevice::ReadOnly | QIODevice::Text)) throw Exception(msgCannotOpenForReading(additionalDocumentationFile)); @@ -923,14 +1217,14 @@ void QtDocGenerator::writeAdditionalDocumentation() const continue; const QString line = QFile::decodeName(lineBA); // Parse "[directory]" specification - if (line.size() > 2 && line.startsWith(QLatin1Char('[')) && line.endsWith(QLatin1Char(']'))) { + if (line.size() > 2 && line.startsWith(u'[') && line.endsWith(u']')) { const QString dir = line.mid(1, line.size() - 2); - if (dir.isEmpty() || dir == QLatin1String(".")) { + if (dir.isEmpty() || dir == u".") { targetDir = outDir.absolutePath(); } else { if (!outDir.exists(dir) && !outDir.mkdir(dir)) { - const QString m = QStringLiteral("Cannot create directory ") - + dir + QStringLiteral(" under ") + const QString m = "Cannot create directory "_L1 + + dir + " under "_L1 + QDir::toNativeSeparators(outputDirectory()); throw Exception(m); } @@ -938,11 +1232,11 @@ void QtDocGenerator::writeAdditionalDocumentation() const } } else { // Normal file entry - QFileInfo fi(m_parameters.docDataDir + QLatin1Char('/') + line); + QFileInfo fi(m_options.parameters.docDataDir + u'/' + line); if (fi.isFile()) { const QString rstFileName = fi.baseName() + rstSuffix; - const QString rstFile = targetDir + QLatin1Char('/') + rstFileName; - const QString context = targetDir.mid(targetDir.lastIndexOf(QLatin1Char('/')) + 1); + const QString rstFile = targetDir + u'/' + rstFileName; + const QString context = targetDir.mid(targetDir.lastIndexOf(u'/') + 1); if (convertToRst(fi.absoluteFilePath(), rstFile, context, &errorMessage)) { ++successCount; @@ -956,7 +1250,7 @@ void QtDocGenerator::writeAdditionalDocumentation() const // FIXME: This should be an exception, in principle, but it // requires building all modules. qCWarning(lcShibokenDoc, "%s", - qPrintable(msgNonExistentAdditionalDocFile(m_parameters.docDataDir, line))); + qPrintable(msgNonExistentAdditionalDocFile(m_options.parameters.docDataDir, line))); } ++count; } @@ -975,79 +1269,133 @@ void QtDocGenerator::writeAdditionalDocumentation() const bool QtDocGenerator::doSetup() { - if (m_parameters.codeSnippetDirs.isEmpty()) { - m_parameters.codeSnippetDirs = - m_parameters.libSourceDir.split(QLatin1Char(PATH_SEP)); + if (m_options.parameters.codeSnippetDirs.isEmpty()) { + m_options.parameters.codeSnippetDirs = + m_options.parameters.libSourceDir.split(QLatin1Char(PATH_SEP)); } - if (m_docParser.isNull()) - m_docParser.reset(new QtDocParser); + if (m_docParser.isNull()) { + if (m_options.doxygen) + m_docParser.reset(new DoxygenParser); + else + m_docParser.reset(new QtDocParser); + } - if (m_parameters.libSourceDir.isEmpty() - || m_parameters.docDataDir.isEmpty()) { + if (m_options.parameters.libSourceDir.isEmpty() + || m_options.parameters.docDataDir.isEmpty()) { qCWarning(lcShibokenDoc) << "Documentation data dir and/or Qt source dir not informed, " "documentation will not be extracted from Qt sources."; return false; } - m_docParser->setDocumentationDataDirectory(m_parameters.docDataDir); - m_docParser->setLibrarySourceDirectory(m_parameters.libSourceDir); - m_parameters.outputDirectory = outputDirectory(); + m_docParser->setDocumentationDataDirectory(m_options.parameters.docDataDir); + m_docParser->setLibrarySourceDirectory(m_options.parameters.libSourceDir); + m_options.parameters.outputDirectory = outputDirectory(); return true; } - -Generator::OptionDescriptions QtDocGenerator::options() const +QList<OptionDescription> QtDocGenerator::options() { return { - {QLatin1String("doc-parser=<parser>"), - QLatin1String("The documentation parser used to interpret the documentation\n" - "input files (qdoc|doxygen)")}, - {QLatin1String("documentation-code-snippets-dir=<dir>"), - QLatin1String("Directory used to search code snippets used by the documentation")}, - {QLatin1String("documentation-data-dir=<dir>"), - QLatin1String("Directory with XML files generated by documentation tool")}, - {QLatin1String("documentation-extra-sections-dir=<dir>"), - QLatin1String("Directory used to search for extra documentation sections")}, - {QLatin1String("library-source-dir=<dir>"), - QLatin1String("Directory where library source code is located")}, - {additionalDocumentationOption() + QLatin1String("=<file>"), - QLatin1String("List of additional XML files to be converted to .rst files\n" - "(for example, tutorials).")} + {u"doc-parser=<parser>"_s, + u"The documentation parser used to interpret the documentation\n" + "input files (qdoc|doxygen)"_s}, + {u"documentation-code-snippets-dir=<dir>"_s, + u"Directory used to search code snippets used by the documentation"_s}, + {u"snippets-path-rewrite=old:new"_s, + u"Replacements in code snippet path to find .cpp/.h snippets converted to Python"_s}, + {u"documentation-data-dir=<dir>"_s, + u"Directory with XML files generated by documentation tool"_s}, + {u"documentation-extra-sections-dir=<dir>"_s, + u"Directory used to search for extra documentation sections"_s}, + {u"library-source-dir=<dir>"_s, + u"Directory where library source code is located"_s}, + {additionalDocumentationOption + u"=<file>"_s, + u"List of additional XML files to be converted to .rst files\n" + "(for example, tutorials)."_s}, + {u"inheritance-file=<file>"_s, + u"Generate a JSON file containing the class inheritance."_s}, + {u"disable-inheritance-diagram"_s, + u"Disable the generation of the inheritance diagram."_s} }; } -bool QtDocGenerator::handleOption(const QString &key, const QString &value) +class QtDocGeneratorOptionsParser : public OptionsParser +{ +public: + explicit QtDocGeneratorOptionsParser(DocGeneratorOptions *o) : m_options(o) {} + + bool handleBoolOption(const QString &key, OptionSource source) override; + bool handleOption(const QString &key, const QString &value, OptionSource source) override; + +private: + DocGeneratorOptions *m_options; +}; + +bool QtDocGeneratorOptionsParser::handleBoolOption(const QString &key, OptionSource) +{ + if (key == "disable-inheritance-diagram"_L1) { + m_options->inheritanceDiagram = false; + return true; + } + return false; +} + +bool QtDocGeneratorOptionsParser::handleOption(const QString &key, const QString &value, + OptionSource source) { - if (key == QLatin1String("library-source-dir")) { - m_parameters.libSourceDir = value; + if (source == OptionSource::CommandLineSingleDash) + return false; + if (key == u"library-source-dir") { + m_options->parameters.libSourceDir = value; return true; } - if (key == QLatin1String("documentation-data-dir")) { - m_parameters.docDataDir = value; + if (key == u"documentation-data-dir") { + m_options->parameters.docDataDir = value; return true; } - if (key == QLatin1String("documentation-code-snippets-dir")) { - m_parameters.codeSnippetDirs = value.split(QLatin1Char(PATH_SEP)); + if (key == u"documentation-code-snippets-dir") { + m_options->parameters.codeSnippetDirs = value.split(QLatin1Char(PATH_SEP)); return true; } - if (key == QLatin1String("documentation-extra-sections-dir")) { - m_extraSectionDir = value; + + if (key == u"snippets-path-rewrite") { + const auto pos = value.indexOf(u':'); + if (pos == -1) + return false; + m_options->parameters.codeSnippetRewriteOld= value.left(pos); + m_options->parameters.codeSnippetRewriteNew = value.mid(pos + 1); return true; } - if (key == QLatin1String("doc-parser")) { + + if (key == u"documentation-extra-sections-dir") { + m_options->extraSectionDir = value; + return true; + } + if (key == u"doc-parser") { qCDebug(lcShibokenDoc).noquote().nospace() << "doc-parser: " << value; - if (value == QLatin1String("doxygen")) - m_docParser.reset(new DoxygenParser); + if (value == u"doxygen") + m_options->doxygen = true; return true; } - if (key == additionalDocumentationOption()) { - m_additionalDocumentationList = value; + if (key == additionalDocumentationOption) { + m_options->additionalDocumentationList = value; return true; } + + if (key == u"inheritance-file") { + m_options->inheritanceFile = value; + return true; + } + return false; } +std::shared_ptr<OptionsParser> QtDocGenerator::createOptionsParser() +{ + return std::make_shared<QtDocGeneratorOptionsParser>(&m_options); +} + bool QtDocGenerator::convertToRst(const QString &sourceFileName, const QString &targetFileName, const QString &context, @@ -1063,20 +1411,65 @@ bool QtDocGenerator::convertToRst(const QString &sourceFileName, sourceFile.close(); FileOut targetFile(targetFileName); - QtXmlToSphinx x(this, m_parameters, doc, context); + QtXmlToSphinx x(this, m_options.parameters, doc, context); targetFile.stream << x; targetFile.done(); return true; } +GeneratorDocumentation + QtDocGenerator::generatorDocumentation(const AbstractMetaClassCPtr &cppClass) +{ + GeneratorDocumentation result; + const auto allFunctions = cppClass->functions(); + result.allFunctions.reserve(allFunctions.size()); + std::remove_copy_if(allFunctions.cbegin(), allFunctions.cend(), + std::back_inserter(result.allFunctions), shouldSkip); + + std::stable_sort(result.allFunctions.begin(), result.allFunctions.end(), functionSort); + + for (const auto &func : std::as_const(result.allFunctions)) { + if (func->isStatic()) + result.tocStaticFunctions.append(func); + else if (func->isVirtual()) + result.tocVirtuals.append(func); + else if (func->isSignal()) + result.tocSignalFunctions.append(func); + else if (func->isSlot()) + result.tocSlotFunctions.append(func); + else + result.tocNormalFunctions.append(func); + } + + // Find the property getters/setters + for (const auto &spec: cppClass->propertySpecs()) { + GeneratorDocumentation::Property property; + property.name = spec.name(); + property.type = spec.type(); + property.documentation = spec.documentation(); + if (!spec.read().isEmpty()) + property.getter = AbstractMetaFunction::find(result.allFunctions, spec.read()); + if (!spec.write().isEmpty()) + property.setter = AbstractMetaFunction::find(result.allFunctions, spec.write()); + if (!spec.reset().isEmpty()) + property.reset = AbstractMetaFunction::find(result.allFunctions, spec.reset()); + if (!spec.notify().isEmpty()) + property.notify = AbstractMetaFunction::find(result.tocSignalFunctions, spec.notify()); + result.properties.append(property); + } + std::sort(result.properties.begin(), result.properties.end()); + + return result; +} + // QtXmlToSphinxDocGeneratorInterface QString QtDocGenerator::expandFunction(const QString &function) const { - const int firstDot = function.indexOf(QLatin1Char('.')); - const AbstractMetaClass *metaClass = nullptr; + const auto firstDot = function.indexOf(u'.'); + AbstractMetaClassCPtr metaClass; if (firstDot != -1) { const auto className = QStringView{function}.left(firstDot); - for (auto cls : api().classes()) { + for (const auto &cls : api().classes()) { if (cls->name() == className) { metaClass = cls; break; @@ -1097,12 +1490,11 @@ QString QtDocGenerator::expandClass(const QString &context, return typeEntry->qualifiedTargetLangName(); // fall back to the old heuristic if the type wasn't found. QString result = name; - const auto rawlinklist = QStringView{name}.split(QLatin1Char('.')); - QStringList splittedContext = context.split(QLatin1Char('.')); + const auto rawlinklist = QStringView{name}.split(u'.'); + QStringList splittedContext = context.split(u'.'); if (rawlinklist.size() == 1 || rawlinklist.constFirst() == splittedContext.constLast()) { splittedContext.removeLast(); - result.prepend(QLatin1Char('~') + splittedContext.join(QLatin1Char('.')) - + QLatin1Char('.')); + result.prepend(u'~' + splittedContext.join(u'.') + u'.'); } return result; } @@ -1110,10 +1502,10 @@ QString QtDocGenerator::expandClass(const QString &context, QString QtDocGenerator::resolveContextForMethod(const QString &context, const QString &methodName) const { - const auto currentClass = QStringView{context}.split(QLatin1Char('.')).constLast(); + const auto currentClass = QStringView{context}.split(u'.').constLast(); - const AbstractMetaClass *metaClass = nullptr; - for (auto cls : api().classes()) { + AbstractMetaClassCPtr metaClass; + for (const auto &cls : api().classes()) { if (cls->name() == currentClass) { metaClass = cls; break; @@ -1128,8 +1520,8 @@ QString QtDocGenerator::resolveContextForMethod(const QString &context, funcList.append(func); } - const AbstractMetaClass *implementingClass = nullptr; - for (const auto &func : qAsConst(funcList)) { + AbstractMetaClassCPtr implementingClass; + for (const auto &func : std::as_const(funcList)) { implementingClass = func->implementingClass(); if (implementingClass->name() == currentClass) break; @@ -1139,7 +1531,7 @@ QString QtDocGenerator::resolveContextForMethod(const QString &context, return implementingClass->typeEntry()->qualifiedTargetLangName(); } - return QLatin1Char('~') + context; + return u'~' + context; } const QLoggingCategory &QtDocGenerator::loggingCategory() const @@ -1159,8 +1551,8 @@ QtXmlToSphinxLink QtDocGenerator::resolveLink(const QtXmlToSphinxLink &link) con { if (link.type != QtXmlToSphinxLink::Reference || !isRelativeHtmlFile(link.linkRef)) return link; - static const QString prefix = QStringLiteral("https://doc.qt.io/qt-") - + QString::number(QT_VERSION_MAJOR) + QLatin1Char('/'); + static const QString prefix = "https://doc.qt.io/qt-"_L1 + + QString::number(QT_VERSION_MAJOR) + u'/'; QtXmlToSphinxLink resolved = link; resolved.type = QtXmlToSphinxLink::External; resolved.linkRef = prefix + link.linkRef; @@ -1170,6 +1562,5 @@ QtXmlToSphinxLink QtDocGenerator::resolveLink(const QtXmlToSphinxLink &link) con if (anchor != -1) resolved.linkText.truncate(anchor); } - qDebug() << __FUNCTION__ << link << "->" << resolved; return resolved; } diff --git a/sources/shiboken6/generator/qtdoc/qtdocgenerator.h b/sources/shiboken6/generator/qtdoc/qtdocgenerator.h index ab2f2fd45..3b1c82e74 100644 --- a/sources/shiboken6/generator/qtdoc/qtdocgenerator.h +++ b/sources/shiboken6/generator/qtdoc/qtdocgenerator.h @@ -1,30 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2020 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of Qt for Python. -** -** $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$ -** -****************************************************************************/ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #ifndef DOCGENERATOR_H #define DOCGENERATOR_H @@ -34,11 +9,15 @@ #include "generator.h" #include "documentation.h" +#include <optionsparser.h> #include "typesystem_enums.h" -#include "typesystem_typedefs.h" +#include "modifications_typedefs.h" #include "qtxmltosphinxinterface.h" class DocParser; +struct DocGeneratorOptions; +struct GeneratorDocumentation; +struct DocPackage; /** * The DocGenerator generates documentation from library being binded. @@ -46,6 +25,8 @@ class DocParser; class QtDocGenerator : public Generator, public QtXmlToSphinxDocGeneratorInterface { public: + Q_DISABLE_COPY_MOVE(QtDocGenerator) + QtDocGenerator(); ~QtDocGenerator(); @@ -56,8 +37,8 @@ public: return "QtDocGenerator"; } - OptionDescriptions options() const override; - bool handleOption(const QString &key, const QString &value) override; + static QList<OptionDescription> options(); + static std::shared_ptr<OptionsParser> createOptionsParser(); // QtXmlToSphinxDocGeneratorInterface QString expandFunction(const QString &function) const override; @@ -68,63 +49,81 @@ public: const QLoggingCategory &loggingCategory() const override; QtXmlToSphinxLink resolveLink(const QtXmlToSphinxLink &) const override; + static QString getFuncName(const AbstractMetaFunctionCPtr &cppFunc); + static QString formatArgs(const AbstractMetaFunctionCPtr &func); + protected: - bool shouldGenerate(const AbstractMetaClass *) const override; - QString fileNameSuffix() const override; + bool shouldGenerate(const TypeEntryCPtr &) const override; + static QString fileNameSuffix(); QString fileNameForContext(const GeneratorContext &context) const override; void generateClass(TextStream &ts, const GeneratorContext &classContext) override; bool finishGeneration() override; private: - void writeEnums(TextStream& s, const AbstractMetaClass* cppClass) const; - - void writeFields(TextStream &s, const AbstractMetaClass *cppClass) const; - static QString functionSignature(const AbstractMetaClass* cppClass, - const AbstractMetaFunctionCPtr &func); - void writeFunction(TextStream& s, const AbstractMetaClass* cppClass, - const AbstractMetaFunctionCPtr &func, bool indexed = true); - void writeFunctionParametersType(TextStream &s, const AbstractMetaClass *cppClass, + void writeEnums(TextStream &s, const AbstractMetaEnumList &enums, + const QString &scope) const; + + void writeFields(TextStream &s, const AbstractMetaClassCPtr &cppClass) const; + void writeFunctions(TextStream &s, const AbstractMetaFunctionCList &funcs, + const AbstractMetaClassCPtr &cppClass, const QString &scope); + void writeFunction(TextStream &s, const AbstractMetaFunctionCPtr &func, + const AbstractMetaClassCPtr &cppClass = {}, + const QString &scope = {}, bool indexed = true); + void writeFunctionDocumentation(TextStream &s, const AbstractMetaFunctionCPtr &func, + const DocModificationList &modifications, + const QString &scope) const; + void writeFunctionParametersType(TextStream &s, const AbstractMetaClassCPtr &cppClass, const AbstractMetaFunctionCPtr &func) const; - static void writeFunctionList(TextStream& s, const AbstractMetaClass* cppClass); - static void writeFunctionBlock(TextStream& s, const QString& title, - QStringList& functions); - void writeParameterType(TextStream &s, const AbstractMetaClass *cppClass, + static void writeFunctionToc(TextStream &s, const QString &title, + const AbstractMetaFunctionCList &functions); + static void writePropertyToc(TextStream &s, + const GeneratorDocumentation &doc); + void writeProperties(TextStream &s, + const GeneratorDocumentation &doc, + const AbstractMetaClassCPtr &cppClass) const; + void writeParameterType(TextStream &s, const AbstractMetaClassCPtr &cppClass, const AbstractMetaArgument &arg) const; - - void writeConstructors(TextStream &s, const AbstractMetaClass *cppClass) const; - void writeFormattedText(TextStream &s, const QString &doc, Documentation::Format format, - const AbstractMetaClass *metaClass = nullptr) const; + const QString &scope = {}) const; void writeFormattedBriefText(TextStream &s, const Documentation &doc, - const AbstractMetaClass *metaclass = nullptr) const; + const QString &scope = {}) const; void writeFormattedDetailedText(TextStream &s, const Documentation &doc, - const AbstractMetaClass *metaclass = nullptr) const; - - bool writeInjectDocumentation(TextStream& s, TypeSystem::DocModificationMode mode, - const AbstractMetaClass* cppClass, - const AbstractMetaFunctionCPtr &func); + const QString &scope = {}) const; + + bool writeInjectDocumentation(TextStream &s, TypeSystem::DocModificationMode mode, + const AbstractMetaClassCPtr &cppClass) const; + bool writeInjectDocumentation(TextStream &s, TypeSystem::DocModificationMode mode, + const DocModificationList &modifications, + const AbstractMetaFunctionCPtr &func, + const QString &scope = {}) const; + bool writeDocModifications(TextStream &s, const DocModificationList &mods, + TypeSystem::DocModificationMode mode, + const QString &scope = {}) const; static void writeDocSnips(TextStream &s, const CodeSnipList &codeSnips, TypeSystem::CodeSnipPosition position, TypeSystem::Language language); void writeModuleDocumentation(); + void writeGlobals(const QString &package, const QString &fileName, + const DocPackage &docPackage); void writeAdditionalDocumentation() const; + bool writeInheritanceFile(); - static QString parseArgDocStyle(const AbstractMetaClass *cppClass, - const AbstractMetaFunctionCPtr &func); - QString translateToPythonType(const AbstractMetaType &type, const AbstractMetaClass *cppClass) const; + QString translateToPythonType(const AbstractMetaType &type, + const AbstractMetaClassCPtr &cppClass, + bool createRef = true) const; bool convertToRst(const QString &sourceFileName, const QString &targetFileName, const QString &context = QString(), QString *errorMessage = nullptr) const; - QString m_extraSectionDir; + static GeneratorDocumentation generatorDocumentation(const AbstractMetaClassCPtr &cppClass); + QStringList m_functionList; - QMap<QString, QStringList> m_packages; + QMap<QString, DocPackage> m_packages; QScopedPointer<DocParser> m_docParser; - QtXmlToSphinxParameters m_parameters; - QString m_additionalDocumentationList; + static DocGeneratorOptions m_options; }; #endif // DOCGENERATOR_H diff --git a/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp b/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp index 4607f3f78..55c1d2090 100644 --- a/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp +++ b/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp @@ -1,30 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2020 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of Qt for Python. -** -** $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$ -** -****************************************************************************/ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "qtxmltosphinx.h" #include "exception.h" @@ -32,16 +7,17 @@ #include <codesniphelpers.h> #include "rstformat.h" +#include "qtcompat.h" + #include <QtCore/QDebug> #include <QtCore/QDir> #include <QtCore/QFileInfo> +#include <QtCore/QHash> #include <QtCore/QLoggingCategory> #include <QtCore/QRegularExpression> #include <QtCore/QXmlStreamReader> -static inline QString nameAttribute() { return QStringLiteral("name"); } -static inline QString titleAttribute() { return QStringLiteral("title"); } -static inline QString fullTitleAttribute() { return QStringLiteral("fulltitle"); } +using namespace Qt::StringLiterals; QString msgTagWarning(const QXmlStreamReader &reader, const QString &context, const QString &tag, const QString &message) @@ -59,16 +35,23 @@ QString msgTagWarning(const QXmlStreamReader &reader, const QString &context, return result; } -QString msgFallbackWarning(const QXmlStreamReader &reader, const QString &context, - const QString &tag, const QString &location, - const QString &identifier, const QString &fallback) +QString msgFallbackWarning(const QString &location, const QString &identifier, + const QString &fallback) { - QString message = QLatin1String("Falling back to \"") - + QDir::toNativeSeparators(fallback) + QLatin1String("\" for \"") - + location + QLatin1Char('"'); + QString message = u"Falling back to \""_s + + QDir::toNativeSeparators(fallback) + u"\" for \""_s + + location + u'"'; if (!identifier.isEmpty()) - message += QLatin1String(" [") + identifier + QLatin1Char(']'); - return msgTagWarning(reader, context, tag, message); + message += u" ["_s + identifier + u']'; + return message; +} + +QString msgSnippetsResolveError(const QString &path, const QStringList &locations) +{ + QString result; + QTextStream(&result) << "Could not resolve \"" << path << R"(" in ")" + << locations.join(uR"(", ")"_s); + return result; } static bool isHttpLink(const QString &ref) @@ -76,6 +59,20 @@ static bool isHttpLink(const QString &ref) return ref.startsWith(u"http://") || ref.startsWith(u"https://"); } +static QString trimRight(QString s) +{ + while (!s.isEmpty() && s.crbegin()->isSpace()) + s.chop(1); + return s; +} + +static QString trimLeadingNewlines(QString s) +{ + while (!s.isEmpty() && s.at(0) == u'\n') + s.remove(0, 1); + return s; +} + QDebug operator<<(QDebug d, const QtXmlToSphinxLink &l) { static const QHash<QtXmlToSphinxLink::Type, const char *> typeName = { @@ -99,6 +96,29 @@ QDebug operator<<(QDebug d, const QtXmlToSphinxLink &l) return d; } +QDebug operator<<(QDebug debug, const QtXmlToSphinx::TableCell &c) +{ + QDebugStateSaver saver(debug); + debug.noquote(); + debug.nospace(); + debug << "Cell(\"" << c.data << '"'; + if (c.colSpan != 0) + debug << ", colSpan=" << c.colSpan; + if (c.rowSpan != 0) + debug << ", rowSpan=" << c.rowSpan; + debug << ')'; + return debug; +} + +QDebug operator<<(QDebug debug, const QtXmlToSphinx::Table &t) +{ + QDebugStateSaver saver(debug); + debug.noquote(); + debug.nospace(); + t.formatDebug(debug); + return debug; +} + static const char *linkKeyWord(QtXmlToSphinxLink::Type type) { switch (type) { @@ -133,7 +153,7 @@ TextStream &operator<<(TextStream &str, const QtXmlToSphinxLink &linkContext) const bool isExternal = linkContext.type == QtXmlToSphinxLink::External; if (!linkContext.linkText.isEmpty()) { writeEscapedRstText(str, linkContext.linkText); - if (isExternal && !linkContext.linkText.endsWith(QLatin1Char(' '))) + if (isExternal && !linkContext.linkText.endsWith(u' ')) str << ' '; str << '<'; } @@ -285,7 +305,7 @@ void QtXmlToSphinx::callHandler(WebXmlTag t, QXmlStreamReader &r) handleTableTag(r); break; case WebXmlTag::header: - handleRowTag(r); + handleHeaderTag(r); break; case WebXmlTag::row: handleRowTag(r); @@ -397,33 +417,49 @@ void QtXmlToSphinx::callHandler(WebXmlTag t, QXmlStreamReader &r) void QtXmlToSphinx::formatCurrentTable() { - if (m_currentTable.isEmpty()) + Q_ASSERT(!m_tables.isEmpty()); + auto &table = m_tables.back(); + if (table.isEmpty()) return; - m_currentTable.setHeaderEnabled(m_tableHasHeader); - m_currentTable.normalize(); + table.normalize(); m_output << '\n'; - m_currentTable.format(m_output); + table.format(m_output); } void QtXmlToSphinx::pushOutputBuffer() { - m_buffers.append(StringSharedPtr(new QString{})); - m_output.setString(m_buffers.top().data()); + m_buffers.append(std::make_shared<QString>()); + m_output.setString(m_buffers.top().get()); } QString QtXmlToSphinx::popOutputBuffer() { Q_ASSERT(!m_buffers.isEmpty()); - QString result(*m_buffers.top().data()); + QString result(*m_buffers.top()); m_buffers.pop(); - m_output.setString(m_buffers.isEmpty() ? nullptr : m_buffers.top().data()); + m_output.setString(m_buffers.isEmpty() ? nullptr : m_buffers.top().get()); return result; } +constexpr auto autoTranslatedPlaceholder = "AUTO_GENERATED\n"_L1; +constexpr auto autoTranslatedNote = +R"(.. warning:: + This section contains snippets that were automatically + translated from C++ to Python and may contain errors. + +)"_L1; + +void QtXmlToSphinx::setAutoTranslatedNote(QString *str) const +{ + if (m_containsAutoTranslations) + str->replace(autoTranslatedPlaceholder, autoTranslatedNote); + else + str->remove(autoTranslatedPlaceholder); +} + QString QtXmlToSphinx::transform(const QString& doc) { Q_ASSERT(m_buffers.isEmpty()); - Indentation indentation(m_output); if (doc.trimmed().isEmpty()) return doc; @@ -431,6 +467,9 @@ QString QtXmlToSphinx::transform(const QString& doc) QXmlStreamReader reader(doc); + m_output << autoTranslatedPlaceholder; + Indentation indentation(m_output); + while (!reader.atEnd()) { QXmlStreamReader::TokenType token = reader.readNext(); if (reader.hasError()) { @@ -462,7 +501,7 @@ QString QtXmlToSphinx::transform(const QString& doc) if (!m_inlineImages.isEmpty()) { // Write out inline image definitions stored in handleInlineImageTag(). m_output << '\n' << disableIndent; - for (const InlineImage &img : qAsConst(m_inlineImages)) + for (const InlineImage &img : std::as_const(m_inlineImages)) m_output << ".. |" << img.tag << "| image:: " << img.href << '\n'; m_output << '\n' << enableIndent; m_inlineImages.clear(); @@ -471,13 +510,14 @@ QString QtXmlToSphinx::transform(const QString& doc) m_output.flush(); QString retval = popOutputBuffer(); Q_ASSERT(m_buffers.isEmpty()); + setAutoTranslatedNote(&retval); return retval; } static QString resolveFile(const QStringList &locations, const QString &path) { for (QString location : locations) { - location.append(QLatin1Char('/')); + location.append(u'/'); location.append(path); if (QFileInfo::exists(location)) return location; @@ -485,25 +525,170 @@ static QString resolveFile(const QStringList &locations, const QString &path) return QString(); } -QString QtXmlToSphinx::readFromLocations(const QStringList &locations, const QString &path, - const QString &identifier, QString *errorMessage) +enum class SnippetType { + Other, // .qdoc, .qml,... + CppSource, CppHeader // Potentially converted to Python +}; + +SnippetType snippetType(const QString &path) +{ + if (path.endsWith(u".cpp")) + return SnippetType::CppSource; + if (path.endsWith(u".h")) + return SnippetType::CppHeader; + return SnippetType::Other; +} + +// Return the name of a .cpp/.h snippet converted to Python by snippets-translate +static QString pySnippetName(const QString &path, SnippetType type) +{ + switch (type) { + case SnippetType::CppSource: + return path.left(path.size() - 3) + u"py"_s; + break; + case SnippetType::CppHeader: + return path + u".py"_s; + break; + default: + break; + } + return {}; +} + +QtXmlToSphinx::Snippet QtXmlToSphinx::readSnippetFromLocations(const QString &path, + const QString &identifier, + const QString &fallbackPath, + QString *errorMessage) +{ + // For anything else but C++ header/sources (no conversion to Python), + // use existing fallback paths first. + const auto type = snippetType(path); + if (type == SnippetType::Other && !fallbackPath.isEmpty()) { + const QString code = readFromLocation(fallbackPath, identifier, errorMessage); + return {code, code.isNull() ? Snippet::Error : Snippet::Fallback}; + } + + // For C++ header/sources, try snippets converted to Python first. QString resolvedPath; - if (path.endsWith(QLatin1String(".cpp"))) { - const QString pySnippet = path.left(path.size() - 3) + QLatin1String("py"); - resolvedPath = resolveFile(locations, pySnippet); + const auto &locations = m_parameters.codeSnippetDirs; + + if (type != SnippetType::Other) { + if (!fallbackPath.isEmpty() && !m_parameters.codeSnippetRewriteOld.isEmpty()) { + // Try looking up Python converted snippets by rewriting snippets paths + QString rewrittenPath = pySnippetName(fallbackPath, type); + if (!rewrittenPath.isEmpty()) { + rewrittenPath.replace(m_parameters.codeSnippetRewriteOld, + m_parameters.codeSnippetRewriteNew); + const QString code = readFromLocation(rewrittenPath, identifier, errorMessage); + m_containsAutoTranslations = true; + return {code, code.isNull() ? Snippet::Error : Snippet::Converted}; + } + } + + resolvedPath = resolveFile(locations, pySnippetName(path, type)); + if (!resolvedPath.isEmpty()) { + const QString code = readFromLocation(resolvedPath, identifier, errorMessage); + return {code, code.isNull() ? Snippet::Error : Snippet::Converted}; + } + } + + resolvedPath = resolveFile(locations, path); + if (!resolvedPath.isEmpty()) { + const QString code = readFromLocation(resolvedPath, identifier, errorMessage); + return {code, code.isNull() ? Snippet::Error : Snippet::Resolved}; + } + + if (!fallbackPath.isEmpty()) { + *errorMessage = msgFallbackWarning(path, identifier, fallbackPath); + const QString code = readFromLocation(fallbackPath, identifier, errorMessage); + return {code, code.isNull() ? Snippet::Error : Snippet::Fallback}; } - if (resolvedPath.isEmpty()) - resolvedPath = resolveFile(locations, path); - if (resolvedPath.isEmpty()) { - QTextStream(errorMessage) << "Could not resolve \"" << path << "\" in \"" - << locations.join(QLatin1String("\", \"")); - return QString(); // null + + *errorMessage = msgSnippetsResolveError(path, locations); + return {{}, Snippet::Error}; +} + +// Helpers for extracting qdoc snippets "#/// [id]" +static QString fileNameOfDevice(const QIODevice *inputFile) +{ + const auto *file = qobject_cast<const QFile *>(inputFile); + return file ? QDir::toNativeSeparators(file->fileName()) : u"<stdin>"_s; +} + +static QString msgSnippetNotFound(const QIODevice &inputFile, + const QString &identifier) +{ + return u"Code snippet file found ("_s + fileNameOfDevice(&inputFile) + + u"), but snippet ["_s + identifier + u"] not found."_s; +} + +static QString msgEmptySnippet(const QIODevice &inputFile, int lineNo, + const QString &identifier) +{ + return u"Empty code snippet ["_s + identifier + u"] at "_s + + fileNameOfDevice(&inputFile) + u':' + QString::number(lineNo); +} + +// Pattern to match qdoc snippet IDs with "#/// [id]" comments and helper to find ID +static const QRegularExpression &snippetIdPattern() +{ + static const QRegularExpression result(uR"RX((//|#) *! *\[([^]]+)\])RX"_s); + Q_ASSERT(result.isValid()); + return result; +} + +static bool matchesSnippetId(QRegularExpressionMatchIterator it, + const QString &identifier) +{ + while (it.hasNext()) { + if (it.next().captured(2) == identifier) + return true; + } + return false; +} + +QString QtXmlToSphinx::readSnippet(QIODevice &inputFile, const QString &identifier, + QString *errorMessage) +{ + const QByteArray identifierBA = identifier.toUtf8(); + // Lambda that matches the snippet id + const auto snippetIdPred = [&identifierBA, &identifier](const QByteArray &lineBA) + { + const bool isComment = lineBA.contains('/') || lineBA.contains('#'); + if (!isComment || !lineBA.contains(identifierBA)) + return false; + const QString line = QString::fromUtf8(lineBA); + return matchesSnippetId(snippetIdPattern().globalMatch(line), identifier); + }; + + // Find beginning, skip over + int lineNo = 1; + for (; !inputFile.atEnd() && !snippetIdPred(inputFile.readLine()); + ++lineNo) { + } + + if (inputFile.atEnd()) { + *errorMessage = msgSnippetNotFound(inputFile, identifier); + return {}; + } + + QString code; + for (; !inputFile.atEnd(); ++lineNo) { + const QString line = QString::fromUtf8(inputFile.readLine()); + auto it = snippetIdPattern().globalMatch(line); + if (it.hasNext()) { // Skip snippet id lines + if (matchesSnippetId(it, identifier)) + break; + } else { + code += line; + } } - qCDebug(m_generator->loggingCategory()).noquote().nospace() - << "snippet file " << path - << " [" << identifier << ']' << " resolved to " << resolvedPath; - return readFromLocation(resolvedPath, identifier, errorMessage); + + if (code.isEmpty()) + *errorMessage = msgEmptySnippet(inputFile, lineNo, identifier); + + return code; } QString QtXmlToSphinx::readFromLocation(const QString &location, const QString &identifier, @@ -515,44 +700,18 @@ QString QtXmlToSphinx::readFromLocation(const QString &location, const QString & QTextStream(errorMessage) << "Could not read code snippet file: " << QDir::toNativeSeparators(inputFile.fileName()) << ": " << inputFile.errorString(); - return QString(); // null + return {}; // null } - QString code = QLatin1String(""); // non-null + QString code = u""_s; // non-null if (identifier.isEmpty()) { while (!inputFile.atEnd()) code += QString::fromUtf8(inputFile.readLine()); return CodeSnipHelpers::fixSpaces(code); } - const QRegularExpression searchString(QLatin1String("//!\\s*\\[") - + identifier + QLatin1String("\\]")); - Q_ASSERT(searchString.isValid()); - static const QRegularExpression codeSnippetCode(QLatin1String("//!\\s*\\[[\\w\\d\\s]+\\]")); - Q_ASSERT(codeSnippetCode.isValid()); - - bool getCode = false; - - while (!inputFile.atEnd()) { - QString line = QString::fromUtf8(inputFile.readLine()); - if (getCode && !line.contains(searchString)) { - line.remove(codeSnippetCode); - code += line; - } else if (line.contains(searchString)) { - if (getCode) - break; - getCode = true; - } - } - - if (!getCode) { - QTextStream(errorMessage) << "Code snippet file found (" - << QDir::toNativeSeparators(location) << "), but snippet [" - << identifier << "] not found."; - return QString(); // null - } - - return CodeSnipHelpers::fixSpaces(code); + code = readSnippet(inputFile, identifier, errorMessage); + return code.isEmpty() ? QString{} : CodeSnipHelpers::fixSpaces(code); // maintain isNull() } void QtXmlToSphinx::handleHeadingTag(QXmlStreamReader& reader) @@ -562,7 +721,7 @@ void QtXmlToSphinx::handleHeadingTag(QXmlStreamReader& reader) static char types[] = { '-', '^' }; QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { - uint typeIdx = reader.attributes().value(QLatin1String("level")).toUInt(); + uint typeIdx = reader.attributes().value(u"level"_s).toUInt(); if (typeIdx >= sizeof(types)) type = types[sizeof(types)-1]; else @@ -615,63 +774,97 @@ void QtXmlToSphinx::handleParaTagEnd() { QString result = popOutputBuffer().simplified(); if (result.startsWith(u"**Warning:**")) - result.replace(0, 12, QStringLiteral(".. warning:: ")); + result.replace(0, 12, ".. warning:: "_L1); else if (result.startsWith(u"**Note:**")) - result.replace(0, 9, QStringLiteral(".. note:: ")); + result.replace(0, 9, ".. note:: "_L1); m_output << result << "\n\n"; } void QtXmlToSphinx::handleItalicTag(QXmlStreamReader& reader) { - QXmlStreamReader::TokenType token = reader.tokenType(); - if (token == QXmlStreamReader::StartElement || token == QXmlStreamReader::EndElement) { - m_insideItalic = !m_insideItalic; - m_output << '*'; - } else if (token == QXmlStreamReader::Characters) { + switch (reader.tokenType()) { + case QXmlStreamReader::StartElement: + if (m_formattingDepth++ == 0) { + m_insideItalic = true; + m_output << rstItalic; + } + break; + case QXmlStreamReader::EndElement: + if (--m_formattingDepth == 0) { + m_insideItalic = false; + m_output << rstItalicOff; + } + break; + case QXmlStreamReader::Characters: m_output << escape(reader.text().trimmed()); + break; + default: + break; } } void QtXmlToSphinx::handleBoldTag(QXmlStreamReader& reader) { - QXmlStreamReader::TokenType token = reader.tokenType(); - if (token == QXmlStreamReader::StartElement || token == QXmlStreamReader::EndElement) { - m_insideBold = !m_insideBold; - m_output << "**"; - } else if (token == QXmlStreamReader::Characters) { + switch (reader.tokenType()) { + case QXmlStreamReader::StartElement: + if (m_formattingDepth++ == 0) { + m_insideBold = true; + m_output << rstBold; + } + break; + case QXmlStreamReader::EndElement: + if (--m_formattingDepth == 0) { + m_insideBold = false; + m_output << rstBoldOff; + } + break; + case QXmlStreamReader::Characters: m_output << escape(reader.text().trimmed()); + break; + default: + break; } } void QtXmlToSphinx::handleArgumentTag(QXmlStreamReader& reader) { - QXmlStreamReader::TokenType token = reader.tokenType(); - if (token == QXmlStreamReader::StartElement || token == QXmlStreamReader::EndElement) - m_output << "``"; - else if (token == QXmlStreamReader::Characters) + switch (reader.tokenType()) { + case QXmlStreamReader::StartElement: + if (m_formattingDepth++ == 0) + m_output << rstCode; + break; + case QXmlStreamReader::EndElement: + if (--m_formattingDepth == 0) + m_output << rstCodeOff; + break; + case QXmlStreamReader::Characters: m_output << reader.text().trimmed(); + break; + default: + break; + } } -static inline QString functionLinkType() { return QStringLiteral("function"); } -static inline QString classLinkType() { return QStringLiteral("class"); } +constexpr auto functionLinkType = "function"_L1; +constexpr auto classLinkType = "class"_L1; static inline QString fixLinkType(QStringView type) { // TODO: create a flag PROPERTY-AS-FUNCTION to ask if the properties // are recognized as such or not in the binding - if (type == QLatin1String("property")) - return functionLinkType(); - if (type == QLatin1String("typedef")) - return classLinkType(); + if (type == u"property") + return functionLinkType; + if (type == u"typedef") + return classLinkType; return type.toString(); } static inline QString linkSourceAttribute(const QString &type) { - if (type == functionLinkType() || type == classLinkType()) - return QLatin1String("raw"); - return type == QLatin1String("enum") || type == QLatin1String("page") - ? type : QLatin1String("href"); + if (type == functionLinkType || type == classLinkType) + return u"raw"_s; + return type == u"enum" || type == u"page" + ? type : u"href"_s; } // "See also" links may appear as nested links: @@ -693,8 +886,8 @@ void QtXmlToSphinx::handleSeeAlsoTag(QXmlStreamReader& reader) if (!textR.isEmpty()) { const QString text = textR.toString(); if (m_seeAlsoContext.isNull()) { - const QString type = text.endsWith(QLatin1String("()")) - ? functionLinkType() : classLinkType(); + const QString type = text.endsWith(u"()") + ? functionLinkType : classLinkType; m_seeAlsoContext.reset(handleLinkStart(type, text)); } handleLinkText(m_seeAlsoContext.data(), text); @@ -713,12 +906,12 @@ void QtXmlToSphinx::handleSeeAlsoTag(QXmlStreamReader& reader) } } -static inline QString fallbackPathAttribute() { return QStringLiteral("path"); } +constexpr auto fallbackPathAttribute = "path"_L1; template <class Indent> // const char*/class Indentor void formatSnippet(TextStream &str, Indent indent, const QString &snippet) { - const auto lines = QStringView{snippet}.split(QLatin1Char('\n')); + const auto lines = QStringView{snippet}.split(u'\n'); for (const auto &line : lines) { if (!line.trimmed().isEmpty()) str << indent << line; @@ -744,55 +937,48 @@ void QtXmlToSphinx::handleSnippetTag(QXmlStreamReader& reader) { QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { - const bool consecutiveSnippet = m_lastTagName == QLatin1String("snippet") - || m_lastTagName == QLatin1String("dots") || m_lastTagName == QLatin1String("codeline"); + const bool consecutiveSnippet = m_lastTagName == u"snippet" + || m_lastTagName == u"dots" || m_lastTagName == u"codeline"; if (consecutiveSnippet) { m_output.flush(); - m_output.string()->chop(2); + m_output.string()->chop(1); // Strip newline from previous snippet } - QString location = reader.attributes().value(QLatin1String("location")).toString(); - QString identifier = reader.attributes().value(QLatin1String("identifier")).toString(); + QString location = reader.attributes().value(u"location"_s).toString(); + QString identifier = reader.attributes().value(u"identifier"_s).toString(); + QString fallbackPath; + if (reader.attributes().hasAttribute(fallbackPathAttribute)) + fallbackPath = reader.attributes().value(fallbackPathAttribute).toString(); QString errorMessage; - const QString pythonCode = - readFromLocations(m_parameters.codeSnippetDirs, location, identifier, &errorMessage); + + const Snippet snippet = readSnippetFromLocations(location, identifier, + fallbackPath, &errorMessage); if (!errorMessage.isEmpty()) warn(msgTagWarning(reader, m_context, m_lastTagName, errorMessage)); - // Fall back to C++ snippet when "path" attribute is present. - // Also read fallback snippet when comparison is desired. - QString fallbackCode; - if ((pythonCode.isEmpty() || m_parameters.snippetComparison) - && reader.attributes().hasAttribute(fallbackPathAttribute())) { - const QString fallback = reader.attributes().value(fallbackPathAttribute()).toString(); - if (QFileInfo::exists(fallback)) { - if (pythonCode.isEmpty()) - warn(msgFallbackWarning(reader, m_context, m_lastTagName, location, identifier, fallback)); - fallbackCode = readFromLocation(fallback, identifier, &errorMessage); - if (!errorMessage.isEmpty()) - warn(msgTagWarning(reader, m_context, m_lastTagName, errorMessage)); - } - } - if (!pythonCode.isEmpty() && !fallbackCode.isEmpty() && m_parameters.snippetComparison) - debug(msgSnippetComparison(location, identifier, pythonCode, fallbackCode)); + if (m_parameters.snippetComparison && snippet.result == Snippet::Converted + && !fallbackPath.isEmpty()) { + const QString fallbackCode = readFromLocation(fallbackPath, identifier, &errorMessage); + debug(msgSnippetComparison(location, identifier, snippet.code, fallbackCode)); + } if (!consecutiveSnippet) m_output << "::\n\n"; Indentation indentation(m_output); - const QString code = pythonCode.isEmpty() ? fallbackCode : pythonCode; - if (code.isEmpty()) + if (snippet.result == Snippet::Error) m_output << "<Code snippet \"" << location << ':' << identifier << "\" not found>\n"; else - m_output << code << ensureEndl; + m_output << snippet.code << ensureEndl; m_output << '\n'; } } + void QtXmlToSphinx::handleDotsTag(QXmlStreamReader& reader) { QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { - const bool consecutiveSnippet = m_lastTagName == QLatin1String("snippet") - || m_lastTagName == QLatin1String("dots") || m_lastTagName == QLatin1String("codeline"); + const bool consecutiveSnippet = m_lastTagName == u"snippet" + || m_lastTagName == u"dots" || m_lastTagName == u"codeline"; if (consecutiveSnippet) { m_output.flush(); m_output.string()->chop(2); @@ -800,7 +986,7 @@ void QtXmlToSphinx::handleDotsTag(QXmlStreamReader& reader) m_output << "::\n\n"; } pushOutputBuffer(); - int indent = reader.attributes().value(QLatin1String("indent")).toInt() + int indent = reader.attributes().value(u"indent"_s).toInt() + m_output.indentation() * m_output.tabWidth(); for (int i = 0; i < indent; ++i) m_output << ' '; @@ -817,12 +1003,11 @@ void QtXmlToSphinx::handleTableTag(QXmlStreamReader& reader) if (token == QXmlStreamReader::StartElement) { if (parentTag() == WebXmlTag::para) handleParaTagEnd(); // End <para> to prevent the table from being rst-escaped - m_currentTable.clear(); - m_tableHasHeader = false; + m_tables.push({}); } else if (token == QXmlStreamReader::EndElement) { // write the table on m_output formatCurrentTable(); - m_currentTable.clear(); + m_tables.pop(); if (parentTag() == WebXmlTag::para) handleParaTagStart(); } @@ -834,11 +1019,11 @@ void QtXmlToSphinx::handleTermTag(QXmlStreamReader& reader) if (token == QXmlStreamReader::StartElement) { pushOutputBuffer(); } else if (token == QXmlStreamReader::Characters) { - m_output << reader.text().toString().replace(QLatin1String("::"), QLatin1String(".")); + m_output << reader.text().toString().replace(u"::"_s, u"."_s); } else if (token == QXmlStreamReader::EndElement) { TableCell cell; cell.data = popOutputBuffer().trimmed(); - m_currentTable.appendRow(TableRow(1, cell)); + m_tables.back().appendRow(TableRow(1, cell)); } } @@ -847,70 +1032,83 @@ void QtXmlToSphinx::handleItemTag(QXmlStreamReader& reader) { QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { - if (m_currentTable.isEmpty()) - m_currentTable.appendRow({}); - TableRow& row = m_currentTable.last(); + auto &table = m_tables.back(); + if (table.isEmpty()) + table.appendRow({}); + TableRow& row = table.last(); TableCell cell; - cell.colSpan = reader.attributes().value(QLatin1String("colspan")).toShort(); - cell.rowSpan = reader.attributes().value(QLatin1String("rowspan")).toShort(); + cell.colSpan = reader.attributes().value(u"colspan"_s).toShort(); + cell.rowSpan = reader.attributes().value(u"rowspan"_s).toShort(); row << cell; pushOutputBuffer(); } else if (token == QXmlStreamReader::EndElement) { - QString data = popOutputBuffer().trimmed(); - if (!m_currentTable.isEmpty()) { - TableRow& row = m_currentTable.last(); + QString data = trimLeadingNewlines(trimRight(popOutputBuffer())); + auto &table = m_tables.back(); + if (!table.isEmpty()) { + TableRow& row = table.last(); if (!row.isEmpty()) row.last().data = data; } } } -void QtXmlToSphinx::handleRowTag(QXmlStreamReader& reader) +void QtXmlToSphinx::handleHeaderTag(QXmlStreamReader &reader) { - QXmlStreamReader::TokenType token = reader.tokenType(); - if (token == QXmlStreamReader::StartElement) { - m_tableHasHeader = reader.name() == QLatin1String("header"); - m_currentTable.appendRow({}); + // <header> in WebXML is either a table header or a description of a + // C++ header with "name"/"href" attributes. + if (reader.tokenType() == QXmlStreamReader::StartElement + && !reader.attributes().hasAttribute(u"name"_s)) { + auto &table = m_tables.back(); + table.setHeaderEnabled(true); + table.appendRow({}); } } +void QtXmlToSphinx::handleRowTag(QXmlStreamReader& reader) +{ + if (reader.tokenType() == QXmlStreamReader::StartElement) + m_tables.back().appendRow({}); +} + enum ListType { BulletList, OrderedList, EnumeratedList }; static inline ListType webXmlListType(QStringView t) { - if (t == QLatin1String("enum")) + if (t == u"enum") return EnumeratedList; - if (t == QLatin1String("ordered")) + if (t == u"ordered") return OrderedList; return BulletList; } void QtXmlToSphinx::handleListTag(QXmlStreamReader& reader) { - // BUG We do not support a list inside a table cell static ListType listType = BulletList; QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { - listType = webXmlListType(reader.attributes().value(QLatin1String("type"))); + m_tables.push({}); + auto &table = m_tables.back(); + listType = webXmlListType(reader.attributes().value(u"type"_s)); if (listType == EnumeratedList) { - m_currentTable.appendRow(TableRow{TableCell(QLatin1String("Constant")), - TableCell(QLatin1String("Description"))}); - m_tableHasHeader = true; + table.appendRow(TableRow{TableCell(u"Constant"_s), + TableCell(u"Description"_s)}); + table.setHeaderEnabled(true); } m_output.indent(); } else if (token == QXmlStreamReader::EndElement) { m_output.outdent(); - if (!m_currentTable.isEmpty()) { + const auto &table = m_tables.back(); + if (!table.isEmpty()) { switch (listType) { case BulletList: case OrderedList: { m_output << '\n'; const char *separator = listType == BulletList ? "* " : "#. "; const char *indentLine = listType == BulletList ? " " : " "; - for (const TableCell &cell : m_currentTable.constFirst()) { - const auto itemLines = QStringView{cell.data}.split(QLatin1Char('\n')); + for (const TableCell &cell : table.constFirst()) { + const auto itemLines = QStringView{cell.data}.split(u'\n'); m_output << separator << itemLines.constFirst() << '\n'; - for (int i = 1, max = itemLines.count(); i < max; ++i) + for (qsizetype i = 1, max = itemLines.size(); i < max; ++i) m_output << indentLine << itemLines[i] << '\n'; } m_output << '\n'; @@ -921,7 +1119,7 @@ void QtXmlToSphinx::handleListTag(QXmlStreamReader& reader) break; } } - m_currentTable.clear(); + m_tables.pop(); } } @@ -931,7 +1129,7 @@ void QtXmlToSphinx::handleLinkTag(QXmlStreamReader& reader) case QXmlStreamReader::StartElement: { // <link> embedded in <see-also> means the characters of <see-also> are no link. m_seeAlsoContext.reset(); - const QString type = fixLinkType(reader.attributes().value(QLatin1String("type"))); + const QString type = fixLinkType(reader.attributes().value(u"type"_s)); const QString ref = reader.attributes().value(linkSourceAttribute(type)).toString(); m_linkContext.reset(handleLinkStart(type, ref)); } @@ -952,8 +1150,8 @@ void QtXmlToSphinx::handleLinkTag(QXmlStreamReader& reader) QtXmlToSphinxLink *QtXmlToSphinx::handleLinkStart(const QString &type, QString ref) const { - ref.replace(QLatin1String("::"), QLatin1String(".")); - ref.remove(QLatin1String("()")); + ref.replace(u"::"_s, u"."_s); + ref.remove(u"()"_s); auto *result = new QtXmlToSphinxLink(ref); if (m_insideBold) @@ -963,25 +1161,25 @@ QtXmlToSphinxLink *QtXmlToSphinx::handleLinkStart(const QString &type, QString r if (type == u"external" || isHttpLink(ref)) { result->type = QtXmlToSphinxLink::External; - } else if (type == functionLinkType() && !m_context.isEmpty()) { + } else if (type == functionLinkType && !m_context.isEmpty()) { result->type = QtXmlToSphinxLink::Method; - const auto rawlinklist = QStringView{result->linkRef}.split(QLatin1Char('.')); + const auto rawlinklist = QStringView{result->linkRef}.split(u'.'); if (rawlinklist.size() == 1 || rawlinklist.constFirst() == m_context) { const auto lastRawLink = rawlinklist.constLast().toString(); QString context = m_generator->resolveContextForMethod(m_context, lastRawLink); if (!result->linkRef.startsWith(context)) - result->linkRef.prepend(context + QLatin1Char('.')); + result->linkRef.prepend(context + u'.'); } else { result->linkRef = m_generator->expandFunction(result->linkRef); } - } else if (type == functionLinkType() && m_context.isEmpty()) { + } else if (type == functionLinkType && m_context.isEmpty()) { result->type = QtXmlToSphinxLink::Function; - } else if (type == classLinkType()) { + } else if (type == classLinkType) { result->type = QtXmlToSphinxLink::Class; result->linkRef = m_generator->expandClass(m_context, result->linkRef); - } else if (type == QLatin1String("enum")) { + } else if (type == u"enum") { result->type = QtXmlToSphinxLink::Attribute; - } else if (type == QLatin1String("page")) { + } else if (type == u"page") { // Module, external web page or reference if (result->linkRef == m_parameters.moduleName) result->type = QtXmlToSphinxLink::Module; @@ -995,7 +1193,7 @@ QtXmlToSphinxLink *QtXmlToSphinx::handleLinkStart(const QString &type, QString r // <link raw="Model/View Classes" href="model-view-programming.html#model-view-classes" // type="page" page="Model/View Programming">Model/View Classes</link> -// <link type="page" page="http://doc.qt.io/qt-5/class.html">QML types</link> +// <link type="page" page="https://doc.qt.io/qt-5/class.html">QML types</link> // <link raw="Qt Quick" href="qtquick-index.html" type="page" page="Qt Quick">Qt Quick</link> // <link raw="QObject" href="qobject.html" type="class">QObject</link> // <link raw="Qt::Window" href="qt.html#WindowType-enum" type="enum" enum="Qt::WindowType">Qt::Window</link> @@ -1010,16 +1208,16 @@ static QString fixLinkText(const QtXmlToSphinxLink *linkContext, } // For the language reference documentation, strip the module name. // Clear the link text if that matches the function/class/enumeration name. - const int lastSep = linktext.lastIndexOf(QLatin1String("::")); + const int lastSep = linktext.lastIndexOf(u"::"); if (lastSep != -1) linktext.remove(0, lastSep + 2); else QtXmlToSphinx::stripPythonQualifiers(&linktext); if (linkContext->linkRef == linktext) - return QString(); + return {}; if ((linkContext->type & QtXmlToSphinxLink::FunctionMask) != 0 - && (linkContext->linkRef + QLatin1String("()")) == linktext) { - return QString(); + && (linkContext->linkRef + u"()"_s) == linktext) { + return {}; } return linktext; } @@ -1046,8 +1244,8 @@ static bool copyImage(const QString &href, const QString &docDataDir, const QString &context, const QString &outputDir, const QLoggingCategory &lc, QString *errorMessage) { - const QChar slash = QLatin1Char('/'); - const int lastSlash = href.lastIndexOf(slash); + const QChar slash = u'/'; + const auto lastSlash = href.lastIndexOf(slash); const QString imagePath = lastSlash != -1 ? href.left(lastSlash) : QString(); const QString imageFileName = lastSlash != -1 ? href.right(href.size() - lastSlash - 1) : href; QFileInfo imageSource(docDataDir + slash + href); @@ -1060,10 +1258,10 @@ static bool copyImage(const QString &href, const QString &docDataDir, // FIXME: Not perfect yet, should have knowledge about namespaces (DataVis3D) or // nested classes "Pyside2.QtGui.QTouchEvent.QTouchPoint". QString relativeTargetDir = context; - const int lastDot = relativeTargetDir.lastIndexOf(QLatin1Char('.')); + const auto lastDot = relativeTargetDir.lastIndexOf(u'.'); if (lastDot != -1) relativeTargetDir.truncate(lastDot); - relativeTargetDir.replace(QLatin1Char('.'), slash); + relativeTargetDir.replace(u'.', slash); if (!imagePath.isEmpty()) relativeTargetDir += slash + imagePath; @@ -1111,7 +1309,7 @@ void QtXmlToSphinx::handleImageTag(QXmlStreamReader& reader) { if (reader.tokenType() != QXmlStreamReader::StartElement) return; - const QString href = reader.attributes().value(QLatin1String("href")).toString(); + const QString href = reader.attributes().value(u"href"_s).toString(); if (copyImage(href)) m_output << ".. image:: " << href << "\n\n"; } @@ -1120,17 +1318,17 @@ void QtXmlToSphinx::handleInlineImageTag(QXmlStreamReader& reader) { if (reader.tokenType() != QXmlStreamReader::StartElement) return; - const QString href = reader.attributes().value(QLatin1String("href")).toString(); + const QString href = reader.attributes().value(u"href"_s).toString(); if (!copyImage(href)) return; // Handle inline images by substitution references. Insert a unique tag // enclosed by '|' and define it further down. Determine tag from the base //file name with number. QString tag = href; - int pos = tag.lastIndexOf(QLatin1Char('/')); + auto pos = tag.lastIndexOf(u'/'); if (pos != -1) tag.remove(0, pos + 1); - pos = tag.indexOf(QLatin1Char('.')); + pos = tag.indexOf(u'.'); if (pos != -1) tag.truncate(pos); tag += QString::number(m_inlineImages.size() + 1); @@ -1142,7 +1340,7 @@ void QtXmlToSphinx::handleRawTag(QXmlStreamReader& reader) { QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { - QString format = reader.attributes().value(QLatin1String("format")).toString(); + QString format = reader.attributes().value(u"format"_s).toString(); m_output << ".. raw:: " << format.toLower() << "\n\n"; } else if (token == QXmlStreamReader::Characters) { Indentation indent(m_output); @@ -1195,11 +1393,11 @@ void QtXmlToSphinx::handlePageTag(QXmlStreamReader &reader) m_output << disableIndent; - const auto title = reader.attributes().value(titleAttribute()); + const auto title = reader.attributes().value("title"); if (!title.isEmpty()) m_output << rstLabel(title.toString()); - const auto fullTitle = reader.attributes().value(fullTitleAttribute()); + const auto fullTitle = reader.attributes().value("fulltitle"); const int size = fullTitle.isEmpty() ? writeEscapedRstText(m_output, title) : writeEscapedRstText(m_output, fullTitle); @@ -1212,7 +1410,7 @@ void QtXmlToSphinx::handleTargetTag(QXmlStreamReader &reader) { if (reader.tokenType() != QXmlStreamReader::StartElement) return; - const auto name = reader.attributes().value(nameAttribute()); + const auto name = reader.attributes().value("name"); if (!name.isEmpty()) m_output << rstLabel(name.toString()); } @@ -1232,14 +1430,14 @@ void QtXmlToSphinx::handleAnchorTag(QXmlStreamReader& reader) QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { QString anchor; - if (reader.attributes().hasAttribute(QLatin1String("id"))) - anchor = reader.attributes().value(QLatin1String("id")).toString(); - else if (reader.attributes().hasAttribute(QLatin1String("name"))) - anchor = reader.attributes().value(QLatin1String("name")).toString(); + if (reader.attributes().hasAttribute(u"id"_s)) + anchor = reader.attributes().value(u"id"_s).toString(); + else if (reader.attributes().hasAttribute(u"name"_s)) + anchor = reader.attributes().value(u"name"_s).toString(); if (!anchor.isEmpty() && m_opened_anchor != anchor) { m_opened_anchor = anchor; if (!m_context.isEmpty()) - anchor.prepend(m_context + QLatin1Char('_')); + anchor.prepend(m_context + u'_'); m_output << rstLabel(anchor); } } else if (token == QXmlStreamReader::EndElement) { @@ -1258,7 +1456,7 @@ void QtXmlToSphinx::handleQuoteFileTag(QXmlStreamReader& reader) QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::Characters) { QString location = reader.text().toString(); - location.prepend(m_parameters.libSourceDir + QLatin1Char('/')); + location.prepend(m_parameters.libSourceDir + u'/'); QString errorMessage; QString code = readFromLocation(location, QString(), &errorMessage); if (!errorMessage.isEmpty()) @@ -1273,51 +1471,72 @@ void QtXmlToSphinx::handleQuoteFileTag(QXmlStreamReader& reader) } } +bool QtXmlToSphinx::Table::hasEmptyLeadingRow() const +{ + return !m_rows.isEmpty() && m_rows.constFirst().isEmpty(); +} + +bool QtXmlToSphinx::Table::hasEmptyTrailingRow() const +{ + return !m_rows.isEmpty() && m_rows.constLast().isEmpty(); +} + void QtXmlToSphinx::Table::normalize() { - if (m_normalized || isEmpty()) + if (m_normalized) + return; + + // Empty leading/trailing rows have been observed with nested tables + if (hasEmptyLeadingRow() || hasEmptyLeadingRow()) { + qWarning() << "QtXmlToSphinx: Table with leading/trailing empty columns found: " << *this; + while (hasEmptyTrailingRow()) + m_rows.pop_back(); + while (hasEmptyLeadingRow()) + m_rows.pop_front(); + } + + if (isEmpty()) return; //QDoc3 generates tables with wrong number of columns. We have to //check and if necessary, merge the last columns. - int maxCols = -1; - for (const auto &row : qAsConst(m_rows)) { - if (row.count() > maxCols) - maxCols = row.count(); + qsizetype maxCols = -1; + for (const auto &row : std::as_const(m_rows)) { + if (row.size() > maxCols) + maxCols = row.size(); } if (maxCols <= 0) return; // add col spans - for (int row = 0; row < m_rows.count(); ++row) { - for (int col = 0; col < m_rows.at(row).count(); ++col) { + for (qsizetype row = 0; row < m_rows.size(); ++row) { + for (qsizetype col = 0; col < m_rows.at(row).size(); ++col) { QtXmlToSphinx::TableCell& cell = m_rows[row][col]; bool mergeCols = (col >= maxCols); if (cell.colSpan > 0) { QtXmlToSphinx::TableCell newCell; newCell.colSpan = -1; - for (int i = 0, max = cell.colSpan-1; i < max; ++i) { + for (int i = 0, max = cell.colSpan-1; i < max; ++i) m_rows[row].insert(col + 1, newCell); - } cell.colSpan = 0; col++; } else if (mergeCols) { - m_rows[row][maxCols - 1].data += QLatin1Char(' ') + cell.data; + m_rows[row][maxCols - 1].data += u' ' + cell.data; } } } // row spans - const int numCols = m_rows.constFirst().count(); - for (int col = 0; col < numCols; ++col) { - for (int row = 0; row < m_rows.count(); ++row) { - if (col < m_rows[row].count()) { + const qsizetype numCols = m_rows.constFirst().size(); + for (qsizetype col = 0; col < numCols; ++col) { + for (qsizetype row = 0; row < m_rows.size(); ++row) { + if (col < m_rows[row].size()) { QtXmlToSphinx::TableCell& cell = m_rows[row][col]; if (cell.rowSpan > 0) { QtXmlToSphinx::TableCell newCell; newCell.rowSpan = -1; - int targetRow = row + 1; - const int targetEndRow = - std::min(targetRow + cell.rowSpan - 1, int(m_rows.count())); + qsizetype targetRow = row + 1; + const qsizetype targetEndRow = + std::min(targetRow + cell.rowSpan - 1, m_rows.size()); cell.rowSpan = 0; for ( ; targetRow < targetEndRow; ++targetRow) m_rows[targetRow].insert(col, newCell); @@ -1337,16 +1556,17 @@ void QtXmlToSphinx::Table::format(TextStream& s) const Q_ASSERT(isNormalized()); // calc width and height of each column and row - const int headerColumnCount = m_rows.constFirst().count(); - QList<int> colWidths(headerColumnCount, 0); - QList<int> rowHeights(m_rows.count(), 0); - for (int i = 0, maxI = m_rows.count(); i < maxI; ++i) { + const qsizetype headerColumnCount = m_rows.constFirst().size(); + QList<qsizetype> colWidths(headerColumnCount, 0); + QList<qsizetype> rowHeights(m_rows.size(), 0); + for (qsizetype i = 0, maxI = m_rows.size(); i < maxI; ++i) { const QtXmlToSphinx::TableRow& row = m_rows.at(i); - for (int j = 0, maxJ = std::min(row.count(), colWidths.size()); j < maxJ; ++j) { - const auto rowLines = QStringView{row[j].data}.split(QLatin1Char('\n')); // cache this would be a good idea + for (qsizetype j = 0, maxJ = std::min(row.size(), colWidths.size()); j < maxJ; ++j) { + // cache this would be a good idea + const auto rowLines = QStringView{row[j].data}.split(u'\n'); for (const auto &str : rowLines) - colWidths[j] = std::max(colWidths[j], int(str.size())); - rowHeights[i] = std::max(rowHeights[i], int(rowLines.size())); + colWidths[j] = std::max(colWidths[j], str.size()); + rowHeights[i] = std::max(rowHeights[i], rowLines.size()); } } @@ -1354,44 +1574,41 @@ void QtXmlToSphinx::Table::format(TextStream& s) const return; // empty table (table with empty cells) // create a horizontal line to be used later. - QString horizontalLine = QLatin1String("+"); - for (int i = 0, max = colWidths.count(); i < max; ++i) { - horizontalLine += QString(colWidths.at(i), QLatin1Char('-')); - horizontalLine += QLatin1Char('+'); - } + QString horizontalLine = u"+"_s; + for (auto colWidth : colWidths) + horizontalLine += QString(colWidth, u'-') + u'+'; // write table rows - for (int i = 0, maxI = m_rows.count(); i < maxI; ++i) { // for each row + for (qsizetype i = 0, maxI = m_rows.size(); i < maxI; ++i) { // for each row const QtXmlToSphinx::TableRow& row = m_rows.at(i); // print line s << '+'; - for (int col = 0; col < headerColumnCount; ++col) { - char c; - if (col >= row.length() || row[col].rowSpan == -1) + for (qsizetype col = 0; col < headerColumnCount; ++col) { + char c = '-'; + if (col >= row.size() || row[col].rowSpan == -1) c = ' '; else if (i == 1 && hasHeader()) c = '='; - else - c = '-'; s << Pad(c, colWidths.at(col)) << '+'; } s << '\n'; // Print the table cells - for (int rowLine = 0; rowLine < rowHeights[i]; ++rowLine) { // for each line in a row - int j = 0; - for (int maxJ = std::min(int(row.count()), headerColumnCount); j < maxJ; ++j) { // for each column + for (qsizetype rowLine = 0; rowLine < rowHeights.at(i); ++rowLine) { // for each line in a row + qsizetype j = 0; + for (qsizetype maxJ = std::min(row.size(), headerColumnCount); j < maxJ; ++j) { // for each column const QtXmlToSphinx::TableCell& cell = row[j]; - const auto rowLines = QStringView{cell.data}.split(QLatin1Char('\n')); // FIXME: Cache this!!! + // FIXME: Cache this!!! + const auto rowLines = QStringView{cell.data}.split(u'\n'); if (!j || !cell.colSpan) s << '|'; else s << ' '; - const int width = colWidths.at(j); - if (rowLine < rowLines.count()) + const auto width = int(colWidths.at(j)); + if (rowLine < rowLines.size()) s << AlignedField(rowLines.at(rowLine), width); else s << Pad(' ', width); @@ -1404,9 +1621,31 @@ void QtXmlToSphinx::Table::format(TextStream& s) const s << horizontalLine << "\n\n"; } +void QtXmlToSphinx::Table::formatDebug(QDebug &debug) const +{ + const auto rowCount = m_rows.size(); + debug << "Table(" <<rowCount << " rows"; + if (m_hasHeader) + debug << ", [header]"; + if (m_normalized) + debug << ", [normalized]"; + for (qsizetype r = 0; r < rowCount; ++r) { + const auto &row = m_rows.at(r); + const auto &colCount = row.size(); + debug << ", row " << r << " [" << colCount << "]={"; + for (qsizetype c = 0; c < colCount; ++c) { + if (c > 0) + debug << ", "; + debug << row.at(c); + } + debug << '}'; + } + debug << ')'; +} + void QtXmlToSphinx::stripPythonQualifiers(QString *s) { - const int lastSep = s->lastIndexOf(QLatin1Char('.')); + const int lastSep = s->lastIndexOf(u'.'); if (lastSep != -1) s->remove(0, lastSep + 1); } diff --git a/sources/shiboken6/generator/qtdoc/qtxmltosphinx.h b/sources/shiboken6/generator/qtdoc/qtxmltosphinx.h index 55d6a79b1..398c5bc97 100644 --- a/sources/shiboken6/generator/qtdoc/qtxmltosphinx.h +++ b/sources/shiboken6/generator/qtdoc/qtxmltosphinx.h @@ -1,42 +1,16 @@ -/**************************************************************************** -** -** Copyright (C) 2020 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of Qt for Python. -** -** $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$ -** -****************************************************************************/ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #ifndef QTXMLTOSPHINX_H #define QTXMLTOSPHINX_H #include <textstream.h> -#include <QtCore/QHash> #include <QtCore/QList> #include <QtCore/QScopedPointer> -#include <QtCore/QSharedPointer> #include <QtCore/QStack> -#include <QtCore/QTextStream> + +#include <memory> QT_BEGIN_NAMESPACE class QDebug; @@ -67,7 +41,7 @@ public: QString data; TableCell(const QString& text = QString()) : data(text) {} - TableCell(const char* text) : data(QLatin1String(text)) {} + TableCell(const char* text) : data(QString::fromLatin1(text)) {} }; using TableRow = QList<TableCell>; @@ -96,20 +70,19 @@ public: return m_normalized; } - void clear() { - m_normalized = false; - m_rows.clear(); - } - void appendRow(const TableRow &row) { m_rows.append(row); } - const TableRow &constFirst() { return m_rows.constFirst(); } + const TableRow &constFirst() const { return m_rows.constFirst(); } TableRow &first() { return m_rows.first(); } TableRow &last() { return m_rows.last(); } void format(TextStream& s) const; + void formatDebug(QDebug &debug) const; private: + bool hasEmptyLeadingRow() const; + bool hasEmptyTrailingRow() const; + QList<TableRow> m_rows; bool m_hasHeader = false; bool m_normalized = false; @@ -128,8 +101,12 @@ public: static void stripPythonQualifiers(QString *s); + // For testing + static QString readSnippet(QIODevice &inputFile, const QString &identifier, + QString *errorMessage); + private: - using StringSharedPtr = QSharedPointer<QString>; + using StringSharedPtr = std::shared_ptr<QString>; QString transform(const QString& doc); @@ -155,6 +132,7 @@ private: // table tagsvoid QtXmlToSphinx::handleValueTag(QXmlStreamReader& reader) void handleTableTag(QXmlStreamReader& reader); + void handleHeaderTag(QXmlStreamReader& reader); void handleRowTag(QXmlStreamReader& reader); void handleItemTag(QXmlStreamReader& reader); void handleRawTag(QXmlStreamReader& reader); @@ -182,21 +160,40 @@ private: QStack<StringSharedPtr> m_buffers; // Maintain address stability since it used in TextStream - Table m_currentTable; + QStack<Table> m_tables; // Stack of tables, used for <table><list> with nested <item> QScopedPointer<QtXmlToSphinxLink> m_linkContext; // for <link> QScopedPointer<QtXmlToSphinxLink> m_seeAlsoContext; // for <see-also>foo()</see-also> - bool m_tableHasHeader = false; QString m_context; const QtXmlToSphinxDocGeneratorInterface *m_generator; const QtXmlToSphinxParameters &m_parameters; + int m_formattingDepth = 0; bool m_insideBold = false; bool m_insideItalic = false; QString m_lastTagName; QString m_opened_anchor; QList<InlineImage> m_inlineImages; - QString readFromLocations(const QStringList &locations, const QString &path, - const QString &identifier, QString *errorMessage); + bool m_containsAutoTranslations = false; + + struct Snippet + { + enum Result { + Converted, // C++ converted to Python + Resolved, // Otherwise resolved in snippet paths + Fallback, // Fallback from XML + Error + }; + + QString code; + Result result; + }; + + void setAutoTranslatedNote(QString *str) const; + + Snippet readSnippetFromLocations(const QString &path, + const QString &identifier, + const QString &fallbackPath, + QString *errorMessage); static QString readFromLocation(const QString &location, const QString &identifier, QString *errorMessage); void pushOutputBuffer(); @@ -213,5 +210,7 @@ inline TextStream& operator<<(TextStream& s, const QtXmlToSphinx& xmlToSphinx) } QDebug operator<<(QDebug d, const QtXmlToSphinxLink &l); +QDebug operator<<(QDebug debug, const QtXmlToSphinx::Table &t); +QDebug operator<<(QDebug debug, const QtXmlToSphinx::TableCell &c); #endif // QTXMLTOSPHINX_H diff --git a/sources/shiboken6/generator/qtdoc/qtxmltosphinxinterface.h b/sources/shiboken6/generator/qtdoc/qtxmltosphinxinterface.h index d4e792d76..16eefad83 100644 --- a/sources/shiboken6/generator/qtdoc/qtxmltosphinxinterface.h +++ b/sources/shiboken6/generator/qtdoc/qtxmltosphinxinterface.h @@ -1,30 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2020 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of Qt for Python. -** -** $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$ -** -****************************************************************************/ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #ifndef QTXMLTOSPHINXINTERFACE_H #define QTXMLTOSPHINXINTERFACE_H @@ -40,6 +15,8 @@ struct QtXmlToSphinxParameters QString outputDirectory; QString libSourceDir; QStringList codeSnippetDirs; + QString codeSnippetRewriteOld; + QString codeSnippetRewriteNew; bool snippetComparison = false; }; diff --git a/sources/shiboken6/generator/qtdoc/rstformat.h b/sources/shiboken6/generator/qtdoc/rstformat.h index d14ecc55d..8af7671fb 100644 --- a/sources/shiboken6/generator/qtdoc/rstformat.h +++ b/sources/shiboken6/generator/qtdoc/rstformat.h @@ -1,30 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2020 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of Qt for Python. -** -** $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$ -** -****************************************************************************/ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #ifndef RSTFORMAT_H #define RSTFORMAT_H @@ -55,28 +30,6 @@ inline QByteArray rstDeprecationNote(const char *what) + what + QByteArrayLiteral(" is deprecated.\n\n"); } -class Pad -{ -public: - explicit Pad(char c, int count) : m_char(c), m_count(count) {} - - void write(TextStream &str) const - { - for (int i = 0; i < m_count; ++i) - str << m_char; - } - -private: - const char m_char; - const int m_count; -}; - -inline TextStream &operator<<(TextStream &str, const Pad &pad) -{ - pad.write(str); - return str; -} - template <class String> inline int writeEscapedRstText(TextStream &str, const String &s) { @@ -116,14 +69,14 @@ inline TextStream &operator<<(TextStream &str, const escape &e) // RST anchor string: Anything else but letters, numbers, '_' or '.' replaced by '-' inline bool isValidRstLabelChar(QChar c) { - return c.isLetterOrNumber() || c == QLatin1Char('_') || c == QLatin1Char('.'); + return c.isLetterOrNumber() || c == u'_' || c == u'.'; } inline QString toRstLabel(QString s) { for (int i = 0, size = s.size(); i < size; ++i) { if (!isValidRstLabelChar(s.at(i))) - s[i] = QLatin1Char('-'); + s[i] = u'-'; } return s; } |