diff options
Diffstat (limited to 'sources/shiboken6/generator/qtdoc')
-rw-r--r-- | sources/shiboken6/generator/qtdoc/qtdocgenerator.cpp | 1591 | ||||
-rw-r--r-- | sources/shiboken6/generator/qtdoc/qtdocgenerator.h | 130 | ||||
-rw-r--r-- | sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp | 1643 | ||||
-rw-r--r-- | sources/shiboken6/generator/qtdoc/qtxmltosphinx.h | 216 | ||||
-rw-r--r-- | sources/shiboken6/generator/qtdoc/qtxmltosphinxinterface.h | 68 | ||||
-rw-r--r-- | sources/shiboken6/generator/qtdoc/rstformat.h | 99 |
6 files changed, 3747 insertions, 0 deletions
diff --git a/sources/shiboken6/generator/qtdoc/qtdocgenerator.cpp b/sources/shiboken6/generator/qtdoc/qtdocgenerator.cpp new file mode 100644 index 000000000..1634a7e83 --- /dev/null +++ b/sources/shiboken6/generator/qtdoc/qtdocgenerator.cpp @@ -0,0 +1,1591 @@ +// 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" +#include "ctypenames.h" +#include "pytypenames.h" +#include <abstractmetaenum.h> +#include <abstractmetafield.h> +#include <abstractmetafunction.h> +#include <abstractmetalang.h> +#include "abstractmetalang_helpers.h" +#include <fileout.h> +#include <messages.h> +#include <modifications.h> +#include <propertyspec.h> +#include <reporthandler.h> +#include <textstream.h> +#include <typedatabase.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> + +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; + +constexpr auto none = "None"_L1; + +static bool shouldSkip(const AbstractMetaFunctionCPtr &func) +{ + if (DocParser::skipForQuery(func)) + return true; + + // Search a const clone (QImage::bits() vs QImage::bits() const) + if (func->isConstant()) + return false; + + const AbstractMetaArgumentList funcArgs = func->arguments(); + const auto &ownerFunctions = func->ownerClass()->functions(); + for (const auto &f : ownerFunctions) { + if (f != func + && f->isConstant() + && f->name() == func->name() + && f->arguments().size() == funcArgs.size()) { + // Compare each argument + bool cloneFound = true; + + const AbstractMetaArgumentList fargs = f->arguments(); + for (qsizetype i = 0, max = funcArgs.size(); i < max; ++i) { + if (funcArgs.at(i).type().typeEntry() != fargs.at(i).type().typeEntry()) { + cloneFound = false; + break; + } + } + if (cloneFound) + return true; + } + } + return false; +} + +static bool functionSort(const AbstractMetaFunctionCPtr &func1, const AbstractMetaFunctionCPtr &func2) +{ + 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 TypeEntryCPtr &te) +{ + if (te) { + const auto version = te->version(); + if (!version.isNull() && version > QVersionNumber(0, 0)) + return version; + } + return {}; +} + +struct docRef +{ + 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_options.parameters.snippetComparison = + ReportHandler::debugLevel() >= ReportHandler::FullDebug; +} + +QtDocGenerator::~QtDocGenerator() = default; + +QString QtDocGenerator::fileNameSuffix() +{ + return u".rst"_s; +} + +bool QtDocGenerator::shouldGenerate(const TypeEntryCPtr &te) const +{ + return Generator::shouldGenerate(te) + && te->type() != TypeEntry::SmartPointerType; +} + +QString QtDocGenerator::fileNameForContext(const GeneratorContext &context) const +{ + return fileNameForContextHelper(context, fileNameSuffix(), + FileNameFlag::UnqualifiedName + | FileNameFlag::KeepCase); +} + +void QtDocGenerator::writeFormattedBriefText(TextStream &s, const Documentation &doc, + const QString &scope) const +{ + writeFormattedText(s, doc.brief(), doc.format(), scope); +} + +void QtDocGenerator::writeFormattedDetailedText(TextStream &s, const Documentation &doc, + const QString &scope) const +{ + writeFormattedText(s, doc.detailed(), doc.format(), scope); +} + +void QtDocGenerator::writeFormattedText(TextStream &s, const QString &doc, + Documentation::Format format, + const QString &scope) const +{ + if (format == Documentation::Native) { + QtXmlToSphinx x(this, m_options.parameters, doc, scope); + s << x; + } else { + 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) { + const auto it = std::find_if(line.cbegin(), line.cend(), + [] (QChar c) { return !c.isSpace(); }); + if (it != line.cend()) + typesystemIndentation = qMin(typesystemIndentation, int(it - line.cbegin())); + } + if (typesystemIndentation == std::numeric_limits<int>::max()) + typesystemIndentation = 0; + for (const auto &line : lines) { + s << (typesystemIndentation > 0 && typesystemIndentation < line.size() + ? line.right(line.size() - typesystemIndentation) : line) + << '\n'; + } + } + + s << '\n'; +} + +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 (const auto &c : allClasses) { + if (c != metaClass && inheritsFrom(c, metaClass)) + res << c; + } + + if (!res.isEmpty()) + writeInheritanceList(s, res, "Inherited by"); +} + +static void writeInheritedFromList(TextStream &s, const AbstractMetaClassCPtr &metaClass) +{ + AbstractMetaClassCList res; + + 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) +{ + AbstractMetaClassCPtr metaClass = classContext.metaClass(); + qCDebug(lcShibokenDoc).noquote().nospace() << "Generating Documentation for " << metaClass->fullName(); + + m_packages[metaClass->package()].classPages << fileNameForContext(classContext); + + m_docParser->setPackageName(metaClass->package()); + m_docParser->fillDocumentation(std::const_pointer_cast<AbstractMetaClass>(metaClass)); + + 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, 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()); + + const auto version = versionOf(metaClass->typeEntry()); + if (!version.isNull()) + s << rstVersionAdded(version); + if (metaClass->attributes().testFlag(AbstractMetaClass::Deprecated)) + s << rstDeprecationNote("class"); + + const GeneratorDocumentation doc = generatorDocumentation(metaClass); + + 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 << "\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"; + + s << '\n' << headline("Detailed Description") << ".. _More:\n"; + + 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); + + if (!doc.properties.isEmpty()) + writeProperties(s, doc, metaClass); + + if (!metaClass->isNamespace()) + writeFields(s, metaClass); + + writeFunctions(s, doc.allFunctions, metaClass, scope); +} + +void QtDocGenerator::writeFunctionToc(TextStream &s, const QString &title, + const AbstractMetaFunctionCList &functions) +{ + 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"; + } +} + +void QtDocGenerator::writePropertyToc(TextStream &s, + const GeneratorDocumentation &doc) +{ + if (doc.properties.isEmpty()) + return; + + 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::writeProperties(TextStream &s, + const GeneratorDocumentation &doc, + const AbstractMetaClassCPtr &cppClass) const +{ + 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 AbstractMetaEnumList &enums, + const QString &scope) const +{ + 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); + } + +} + +void QtDocGenerator::writeFields(TextStream &s, const AbstractMetaClassCPtr &cppClass) const +{ + 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(), scope); + } +} + +QString QtDocGenerator::formatArgs(const AbstractMetaFunctionCPtr &func) +{ + QString ret = u"("_s; + int optArgs = 0; + + const AbstractMetaArgumentList &arguments = func->arguments(); + for (const AbstractMetaArgument &arg : arguments) { + + if (arg.isModifiedRemoved()) + continue; + + bool thisIsoptional = !arg.defaultValueExpression().isEmpty(); + if (optArgs || thisIsoptional) { + ret += u'['; + optArgs++; + } + + if (arg.argumentIndex() > 0) + ret += u", "_s; + + ret += arg.name(); + + if (thisIsoptional) { + QString defValue = arg.defaultValueExpression(); + 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(u"::"_s, u"."_s); + if (defValue == u"nullptr") + defValue = none; + else if (defValue == u"0" && arg.type().isObject()) + defValue = none; + } + ret += u'=' + defValue; + } + } + + ret += QString(optArgs, u']') + u')'; + return ret; +} + +void QtDocGenerator::writeDocSnips(TextStream &s, + const CodeSnipList &codeSnips, + TypeSystem::CodeSnipPosition position, + TypeSystem::Language language) +{ + Indentation indentation(s); + 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) || + !(snip.language & language)) + continue; + + QString code = snip.code(); + while (code.contains(startMarkup) && code.contains(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(u'\n'); + int currentRow = 0; + qsizetype offset = 0; + + for (QString row : rows) { + for (const QString &invalidString : std::as_const(invalidStrings)) + row.remove(invalidString); + + if (row.trimmed().size() == 0) { + if (currentRow == 0) + continue; + s << '\n'; + } + + if (currentRow == 0) { + //find offset + for (auto c : row) { + if (c == u' ') + offset++; + else if (c == u'\n') + offset = 0; + else + break; + } + } + s << QStringView{row}.mid(offset) << '\n'; + currentRow++; + } + + code = code.mid(endBlock+endMarkup.size()); + } + } +} + +bool QtDocGenerator::writeDocModifications(TextStream &s, + const DocModificationList &mods, + TypeSystem::DocModificationMode mode, + const QString &scope) const +{ + bool didSomething = false; + for (const DocModification &mod : mods) { + if (mod.mode() == mode) { + 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'; + + // 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, cppClass->typeEntry()->codeSnips(), pos, TypeSystem::TargetLangCode); + return didSomething; +} + +bool QtDocGenerator::writeInjectDocumentation(TextStream &s, + TypeSystem::DocModificationMode mode, + const DocModificationList &modifications, + const AbstractMetaFunctionCPtr &func, + const QString &scope) const +{ + const bool didSomething = writeDocModifications(s, modifications, mode, scope); + s << '\n'; + + // 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; +} + +static QString inline toRef(const QString &t) +{ + return ":class:`~"_L1 + t + u'`'; +} + +QString QtDocGenerator::translateToPythonType(const AbstractMetaType &type, + const AbstractMetaClassCPtr &cppClass, + bool createRef) const +{ + static const QStringList nativeTypes = + {boolT, floatT, intT, pyObjectT, pyStrT}; + + QString name = type.name(); + if (nativeTypes.contains(name)) + return name; + + 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.constFind(name); + if (found != typeMap.cend()) + return found.value(); + + 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(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]); + } + return strType; + } + + if (auto k = AbstractMetaClass::findClass(api().classes(), type.typeEntry())) + return createRef ? toRef(k->fullName()) : k->name(); + + return createRef ? toRef(name) : name; +} + +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 AbstractMetaClassCPtr &cppClass, + const AbstractMetaFunctionCPtr &func) const +{ + s << '\n'; + const AbstractMetaArgumentList &funcArgs = func->arguments(); + for (const AbstractMetaArgument &arg : funcArgs) { + if (!arg.isModifiedRemoved()) + writeParameterType(s, cppClass, arg); + } + + QString retType; + if (!func->isConstructor()) { + // check if the return type was modified + retType = func->modifiedTypeName(); + if (retType.isEmpty() && !func->isVoid()) + retType = translateToPythonType(func->type(), cppClass); + } + + if (!retType.isEmpty()) + s << ":rtype: " << retType << '\n'; + + s << '\n'; +} + +static bool containsFunctionDirective(const DocModification &dm) +{ + 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->isDeprecated()) + s << rstDeprecationNote("function"); + } + + 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"; + } +} + +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() || key.at(1).isDigit())) { + return key.at(1); // "QClass" -> 'C', "qSin()" -> 'S', 'Q3DSurfaceWidget' -> '3' + } + 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; + 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 = u"**"_s + it.key() + u"**"_s; + row << QtXmlToSphinx::TableCell(charEntry); + 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 = "* :"_L1 + referenceType + ":`"_L1 + item + u'`'; + row << QtXmlToSphinx::TableCell(entry); + } + if (row.size() > 1) + table.appendRow(row); + } + + table.normalize(); + s << '\n' << headline(title) << ".. container:: pysidetoc\n\n"; + table.format(s); +} + +bool QtDocGenerator::finishGeneration() +{ + 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_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() +{ + 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(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" << 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(u'.'); + if (lastIndex >= 0) + moduleName.remove(0, lastIndex + 1); + + // Search for extra-sections + QStringList extraTocEntries; + if (!m_options.extraSectionDir.isEmpty()) { + QDir extraSectionDir(m_options.extraSectionDir); + if (!extraSectionDir.exists()) { + 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 + u".?*.rst"_s; + const auto fileList = + extraSectionDir.entryInfoList({filter}, QDir::Files, QDir::Name); + for (const auto &fi : fileList) + readExtraDoc(fi, moduleName, outputDir, &docPackage, &extraTocEntries); + } + + 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"; + if (hasGlobals) + s << globalsPage << '\n'; + for (const QString &className : std::as_const(docPackage.classPages)) + s << className << '\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_options.extraSectionDir + u'/' + moduleName + + u".rst"_s); + if (moduleDoc.open(QIODevice::ReadOnly | QIODevice::Text)) { + s << moduleDoc.readAll(); + moduleDoc.close(); + } else { + // try the normal way + Documentation moduleDoc = m_docParser->retrieveModuleDocumentation(it.key()); + if (moduleDoc.format() == Documentation::Native) { + QString context = it.key(); + QtXmlToSphinx::stripPythonQualifiers(&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, + const QString &fileName) +{ + QString result; + QTextStream(&result) << "Additional documentation file \"" + << fileName << "\" does not exist in " + << QDir::toNativeSeparators(dir) << '.'; + return result; +} + +void QtDocGenerator::writeAdditionalDocumentation() const +{ + QFile additionalDocumentationFile(m_options.additionalDocumentationList); + if (!additionalDocumentationFile.open(QIODevice::ReadOnly | QIODevice::Text)) + throw Exception(msgCannotOpenForReading(additionalDocumentationFile)); + + QDir outDir(outputDirectory()); + const QString rstSuffix = fileNameSuffix(); + + QString errorMessage; + int successCount = 0; + int count = 0; + + QString targetDir = outDir.absolutePath(); + + while (!additionalDocumentationFile.atEnd()) { + const QByteArray lineBA = additionalDocumentationFile.readLine().trimmed(); + if (lineBA.isEmpty() || lineBA.startsWith('#')) + continue; + const QString line = QFile::decodeName(lineBA); + // Parse "[directory]" specification + if (line.size() > 2 && line.startsWith(u'[') && line.endsWith(u']')) { + const QString dir = line.mid(1, line.size() - 2); + if (dir.isEmpty() || dir == u".") { + targetDir = outDir.absolutePath(); + } else { + if (!outDir.exists(dir) && !outDir.mkdir(dir)) { + const QString m = "Cannot create directory "_L1 + + dir + " under "_L1 + + QDir::toNativeSeparators(outputDirectory()); + throw Exception(m); + } + targetDir = outDir.absoluteFilePath(dir); + } + } else { + // Normal file entry + QFileInfo fi(m_options.parameters.docDataDir + u'/' + line); + if (fi.isFile()) { + const QString rstFileName = fi.baseName() + rstSuffix; + const QString rstFile = targetDir + u'/' + rstFileName; + const QString context = targetDir.mid(targetDir.lastIndexOf(u'/') + 1); + if (convertToRst(fi.absoluteFilePath(), + rstFile, context, &errorMessage)) { + ++successCount; + qCDebug(lcShibokenDoc).nospace().noquote() << __FUNCTION__ + << " converted " << fi.fileName() + << ' ' << rstFileName; + } else { + qCWarning(lcShibokenDoc, "%s", qPrintable(errorMessage)); + } + } else { + // FIXME: This should be an exception, in principle, but it + // requires building all modules. + qCWarning(lcShibokenDoc, "%s", + qPrintable(msgNonExistentAdditionalDocFile(m_options.parameters.docDataDir, line))); + } + ++count; + } + } + additionalDocumentationFile.close(); + + qCInfo(lcShibokenDoc, "Created %d/%d additional documentation files.", + successCount, count); +} + +#ifdef __WIN32__ +# define PATH_SEP ';' +#else +# define PATH_SEP ':' +#endif + +bool QtDocGenerator::doSetup() +{ + if (m_options.parameters.codeSnippetDirs.isEmpty()) { + m_options.parameters.codeSnippetDirs = + m_options.parameters.libSourceDir.split(QLatin1Char(PATH_SEP)); + } + + if (m_docParser.isNull()) { + if (m_options.doxygen) + m_docParser.reset(new DoxygenParser); + else + m_docParser.reset(new QtDocParser); + } + + 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_options.parameters.docDataDir); + m_docParser->setLibrarySourceDirectory(m_options.parameters.libSourceDir); + m_options.parameters.outputDirectory = outputDirectory(); + return true; +} + +QList<OptionDescription> QtDocGenerator::options() +{ + return { + {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} + }; +} + +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 (source == OptionSource::CommandLineSingleDash) + return false; + if (key == u"library-source-dir") { + m_options->parameters.libSourceDir = value; + return true; + } + if (key == u"documentation-data-dir") { + m_options->parameters.docDataDir = value; + return true; + } + if (key == u"documentation-code-snippets-dir") { + m_options->parameters.codeSnippetDirs = value.split(QLatin1Char(PATH_SEP)); + return true; + } + + 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 == 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 == u"doxygen") + m_options->doxygen = true; + return true; + } + 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, + QString *errorMessage) const +{ + QFile sourceFile(sourceFileName); + if (!sourceFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + if (errorMessage) + *errorMessage = msgCannotOpenForReading(sourceFile); + return false; + } + const QString doc = QString::fromUtf8(sourceFile.readAll()); + sourceFile.close(); + + FileOut targetFile(targetFileName); + 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 auto firstDot = function.indexOf(u'.'); + AbstractMetaClassCPtr metaClass; + if (firstDot != -1) { + const auto className = QStringView{function}.left(firstDot); + for (const auto &cls : api().classes()) { + if (cls->name() == className) { + metaClass = cls; + break; + } + } + } + + return metaClass + ? metaClass->typeEntry()->qualifiedTargetLangName() + + function.right(function.size() - firstDot) + : function; +} + +QString QtDocGenerator::expandClass(const QString &context, + const QString &name) const +{ + if (auto typeEntry = TypeDatabase::instance()->findType(name)) + return typeEntry->qualifiedTargetLangName(); + // fall back to the old heuristic if the type wasn't found. + QString result = name; + const auto rawlinklist = QStringView{name}.split(u'.'); + QStringList splittedContext = context.split(u'.'); + if (rawlinklist.size() == 1 || rawlinklist.constFirst() == splittedContext.constLast()) { + splittedContext.removeLast(); + result.prepend(u'~' + splittedContext.join(u'.') + u'.'); + } + return result; +} + +QString QtDocGenerator::resolveContextForMethod(const QString &context, + const QString &methodName) const +{ + const auto currentClass = QStringView{context}.split(u'.').constLast(); + + AbstractMetaClassCPtr metaClass; + for (const auto &cls : api().classes()) { + if (cls->name() == currentClass) { + metaClass = cls; + break; + } + } + + if (metaClass) { + AbstractMetaFunctionCList funcList; + const auto &methods = metaClass->queryFunctionsByName(methodName); + for (const auto &func : methods) { + if (methodName == func->name()) + funcList.append(func); + } + + AbstractMetaClassCPtr implementingClass; + for (const auto &func : std::as_const(funcList)) { + implementingClass = func->implementingClass(); + if (implementingClass->name() == currentClass) + break; + } + + if (implementingClass) + return implementingClass->typeEntry()->qualifiedTargetLangName(); + } + + return u'~' + context; +} + +const QLoggingCategory &QtDocGenerator::loggingCategory() const +{ + return lcShibokenDoc(); +} + +static bool isRelativeHtmlFile(const QString &linkRef) +{ + return !linkRef.startsWith(u"http") + && (linkRef.endsWith(u".html") || linkRef.contains(u".html#")); +} + +// Resolve relative, local .html documents links to doc.qt.io as they +// otherwise will not work and neither be found in the HTML tree. +QtXmlToSphinxLink QtDocGenerator::resolveLink(const QtXmlToSphinxLink &link) const +{ + if (link.type != QtXmlToSphinxLink::Reference || !isRelativeHtmlFile(link.linkRef)) + return link; + 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; + if (resolved.linkText.isEmpty()) { + resolved.linkText = link.linkRef; + const qsizetype anchor = resolved.linkText.lastIndexOf(u'#'); + if (anchor != -1) + resolved.linkText.truncate(anchor); + } + return resolved; +} + +QtXmlToSphinxDocGeneratorInterface::Image + QtDocGenerator::resolveImage(const QString &href, const QString &context) const +{ + QString relativeSourceDir = href; + const QString source = m_options.parameters.docDataDir + u'/' + relativeSourceDir; + if (!QFileInfo::exists(source)) + throw Exception(msgCannotFindImage(href, context,source)); + + // Determine target directory from context, "Pyside2.QtGui.QPainter" ->"Pyside2/QtGui". + // FIXME: Not perfect yet, should have knowledge about namespaces (DataVis3D) or + // nested classes "Pyside2.QtGui.QTouchEvent.QTouchPoint". + QString relativeTargetDir = context; + const auto lastDot = relativeTargetDir.lastIndexOf(u'.'); + if (lastDot != -1) + relativeTargetDir.truncate(lastDot); + relativeTargetDir.replace(u'.', u'/'); + if (!relativeTargetDir.isEmpty()) + relativeTargetDir += u'/'; + relativeTargetDir += href; + + return {relativeSourceDir, relativeTargetDir}; +} diff --git a/sources/shiboken6/generator/qtdoc/qtdocgenerator.h b/sources/shiboken6/generator/qtdoc/qtdocgenerator.h new file mode 100644 index 000000000..56e15e2a1 --- /dev/null +++ b/sources/shiboken6/generator/qtdoc/qtdocgenerator.h @@ -0,0 +1,130 @@ +// 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 + +#include <QtCore/QStringList> +#include <QtCore/QMap> +#include <QtCore/QScopedPointer> + +#include "generator.h" +#include "documentation.h" +#include <optionsparser.h> +#include "typesystem_enums.h" +#include "modifications_typedefs.h" +#include "qtxmltosphinxinterface.h" + +class DocParser; +struct DocGeneratorOptions; +struct GeneratorDocumentation; +struct DocPackage; + +/** +* The DocGenerator generates documentation from library being binded. +*/ +class QtDocGenerator : public Generator, public QtXmlToSphinxDocGeneratorInterface +{ +public: + Q_DISABLE_COPY_MOVE(QtDocGenerator) + + QtDocGenerator(); + ~QtDocGenerator(); + + bool doSetup() override; + + const char* name() const override + { + return "QtDocGenerator"; + } + + static QList<OptionDescription> options(); + static std::shared_ptr<OptionsParser> createOptionsParser(); + + // QtXmlToSphinxDocGeneratorInterface + QString expandFunction(const QString &function) const override; + QString expandClass(const QString &context, + const QString &name) const override; + QString resolveContextForMethod(const QString &context, + const QString &methodName) const override; + const QLoggingCategory &loggingCategory() const override; + QtXmlToSphinxLink resolveLink(const QtXmlToSphinxLink &) const override; + Image resolveImage(const QString &href, const QString &context) const override; + + static QString getFuncName(const AbstractMetaFunctionCPtr &cppFunc); + static QString formatArgs(const AbstractMetaFunctionCPtr &func); + +protected: + 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 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 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 writeFormattedText(TextStream &s, const QString &doc, + Documentation::Format format, + const QString &scope = {}) const; + void writeFormattedBriefText(TextStream &s, const Documentation &doc, + const QString &scope = {}) const; + void writeFormattedDetailedText(TextStream &s, const Documentation &doc, + 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(); + + 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; + + static GeneratorDocumentation generatorDocumentation(const AbstractMetaClassCPtr &cppClass); + + QStringList m_functionList; + QMap<QString, DocPackage> m_packages; + QScopedPointer<DocParser> m_docParser; + static DocGeneratorOptions m_options; +}; + +#endif // DOCGENERATOR_H diff --git a/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp b/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp new file mode 100644 index 000000000..b8fec836c --- /dev/null +++ b/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp @@ -0,0 +1,1643 @@ +// 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" +#include "qtxmltosphinxinterface.h" +#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> + +using namespace Qt::StringLiterals; + +QString msgTagWarning(const QXmlStreamReader &reader, const QString &context, + const QString &tag, const QString &message) +{ + QString result; + QTextStream str(&result); + str << "While handling <"; + const auto currentTag = reader.name(); + if (currentTag.isEmpty()) + str << tag; + else + str << currentTag; + str << "> in " << context << ", line "<< reader.lineNumber() + << ": " << message; + return result; +} + +QString msgFallbackWarning(const QString &location, const QString &identifier, + const QString &fallback) +{ + QString message = u"Falling back to \""_s + + QDir::toNativeSeparators(fallback) + u"\" for \""_s + + location + u'"'; + if (!identifier.isEmpty()) + 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) +{ + 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 = { + {QtXmlToSphinxLink::Method, "Method"}, + {QtXmlToSphinxLink::Function, "Function"}, + {QtXmlToSphinxLink::Class, "Class"}, + {QtXmlToSphinxLink::Attribute, "Attribute"}, + {QtXmlToSphinxLink::Module, "Module"}, + {QtXmlToSphinxLink::Reference, "Reference"}, + {QtXmlToSphinxLink::External, "External"}, + }; + + QDebugStateSaver saver(d); + d.noquote(); + d.nospace(); + d << "QtXmlToSphinxLinkContext(" << typeName.value(l.type, "") << ", ref=\"" + << l.linkRef << '"'; + if (!l.linkText.isEmpty()) + d << ", text=\"" << l.linkText << '"'; + d << ')'; + 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) { + case QtXmlToSphinxLink::Method: + return ":meth:"; + case QtXmlToSphinxLink::Function: + return ":func:"; + case QtXmlToSphinxLink::Class: + return ":class:"; + case QtXmlToSphinxLink::Attribute: + return ":attr:"; + case QtXmlToSphinxLink::Module: + return ":mod:"; + case QtXmlToSphinxLink::Reference: + return ":ref:"; + case QtXmlToSphinxLink::External: + break; + case QtXmlToSphinxLink::FunctionMask: + break; + } + return ""; +} + +TextStream &operator<<(TextStream &str, const QtXmlToSphinxLink &linkContext) +{ + // Temporarily turn off bold/italic since links do not work within + if (linkContext.flags & QtXmlToSphinxLink::InsideBold) + str << "**"; + else if (linkContext.flags & QtXmlToSphinxLink::InsideItalic) + str << '*'; + str << ' ' << linkKeyWord(linkContext.type) << '`'; + const bool isExternal = linkContext.type == QtXmlToSphinxLink::External; + if (!linkContext.linkText.isEmpty()) { + writeEscapedRstText(str, linkContext.linkText); + if (isExternal && !linkContext.linkText.endsWith(u' ')) + str << ' '; + str << '<'; + } + // Convert page titles to RST labels + str << (linkContext.type == QtXmlToSphinxLink::Reference + ? toRstLabel(linkContext.linkRef) : linkContext.linkRef); + if (!linkContext.linkText.isEmpty()) + str << '>'; + str << '`'; + if (isExternal) + str << '_'; + str << ' '; + if (linkContext.flags & QtXmlToSphinxLink::InsideBold) + str << "**"; + else if (linkContext.flags & QtXmlToSphinxLink::InsideItalic) + str << '*'; + return str; +} + +enum class WebXmlTag { + Unknown, + heading, brief, para, italic, bold, see_also, snippet, dots, codeline, + table, header, row, item, argument, teletype, link, inlineimage, image, + list, term, raw, underline, superscript, code, badcode, legalese, + rst, section, quotefile, + // ignored tags + generatedlist, tableofcontents, quotefromfile, skipto, target, page, group, + // useless tags + description, definition, printuntil, relation, + // Doxygen tags + title, ref, computeroutput, detaileddescription, name, listitem, + parametername, parameteritem, ulink, itemizedlist, parameternamelist, + parameterlist, + // Doxygen ignored tags + highlight, linebreak, programlisting, xreftitle, sp, entry, simplesect, + verbatim, xrefsect, xrefdescription, +}; + +using WebXmlTagHash = QHash<QStringView, WebXmlTag>; + +static const WebXmlTagHash &webXmlTagHash() +{ + static const WebXmlTagHash result = { + {u"heading", WebXmlTag::heading}, + {u"brief", WebXmlTag::brief}, + {u"para", WebXmlTag::para}, + {u"italic", WebXmlTag::italic}, + {u"bold", WebXmlTag::bold}, + {u"see-also", WebXmlTag::see_also}, + {u"snippet", WebXmlTag::snippet}, + {u"dots", WebXmlTag::dots}, + {u"codeline", WebXmlTag::codeline}, + {u"table", WebXmlTag::table}, + {u"header", WebXmlTag::header}, + {u"row", WebXmlTag::row}, + {u"item", WebXmlTag::item}, + {u"argument", WebXmlTag::argument}, + {u"teletype", WebXmlTag::teletype}, + {u"link", WebXmlTag::link}, + {u"inlineimage", WebXmlTag::inlineimage}, + {u"image", WebXmlTag::image}, + {u"list", WebXmlTag::list}, + {u"term", WebXmlTag::term}, + {u"raw", WebXmlTag::raw}, + {u"underline", WebXmlTag::underline}, + {u"superscript", WebXmlTag::superscript}, + {u"code", WebXmlTag::code}, + {u"badcode", WebXmlTag::badcode}, + {u"legalese", WebXmlTag::legalese}, + {u"rst", WebXmlTag::rst}, + {u"section", WebXmlTag::section}, + {u"quotefile", WebXmlTag::quotefile}, + {u"generatedlist", WebXmlTag::generatedlist}, + {u"tableofcontents", WebXmlTag::tableofcontents}, + {u"quotefromfile", WebXmlTag::quotefromfile}, + {u"skipto", WebXmlTag::skipto}, + {u"target", WebXmlTag::target}, + {u"page", WebXmlTag::page}, + {u"group", WebXmlTag::group}, + {u"description", WebXmlTag::description}, + {u"definition", WebXmlTag::definition}, + {u"printuntil", WebXmlTag::printuntil}, + {u"relation", WebXmlTag::relation}, + {u"title", WebXmlTag::title}, + {u"ref", WebXmlTag::ref}, + {u"computeroutput", WebXmlTag::computeroutput}, + {u"detaileddescription", WebXmlTag::detaileddescription}, + {u"name", WebXmlTag::name}, + {u"listitem", WebXmlTag::listitem}, + {u"parametername", WebXmlTag::parametername}, + {u"parameteritem", WebXmlTag::parameteritem}, + {u"ulink", WebXmlTag::ulink}, + {u"itemizedlist", WebXmlTag::itemizedlist}, + {u"parameternamelist", WebXmlTag::parameternamelist}, + {u"parameterlist", WebXmlTag::parameterlist}, + {u"highlight", WebXmlTag::highlight}, + {u"linebreak", WebXmlTag::linebreak}, + {u"programlisting", WebXmlTag::programlisting}, + {u"xreftitle", WebXmlTag::xreftitle}, + {u"sp", WebXmlTag::sp}, + {u"entry", WebXmlTag::entry}, + {u"simplesect", WebXmlTag::simplesect}, + {u"verbatim", WebXmlTag::verbatim}, + {u"xrefsect", WebXmlTag::xrefsect}, + {u"xrefdescription", WebXmlTag::xrefdescription}, + }; + return result; +} + +QtXmlToSphinx::QtXmlToSphinx(const QtXmlToSphinxDocGeneratorInterface *docGenerator, + const QtXmlToSphinxParameters ¶meters, + const QString& doc, const QString& context) + : m_output(static_cast<QString *>(nullptr)), + m_context(context), + m_generator(docGenerator), m_parameters(parameters) +{ + m_result = transform(doc); +} + +QtXmlToSphinx::~QtXmlToSphinx() = default; + +void QtXmlToSphinx::callHandler(WebXmlTag t, QXmlStreamReader &r) +{ + switch (t) { + case WebXmlTag::heading: + handleHeadingTag(r); + break; + case WebXmlTag::brief: + case WebXmlTag::para: + handleParaTag(r); + break; + case WebXmlTag::italic: + handleItalicTag(r); + break; + case WebXmlTag::bold: + handleBoldTag(r); + break; + case WebXmlTag::see_also: + handleSeeAlsoTag(r); + break; + case WebXmlTag::snippet: + handleSnippetTag(r); + break; + case WebXmlTag::dots: + case WebXmlTag::codeline: + handleDotsTag(r); + break; + case WebXmlTag::table: + handleTableTag(r); + break; + case WebXmlTag::header: + handleHeaderTag(r); + break; + case WebXmlTag::row: + handleRowTag(r); + break; + case WebXmlTag::item: + handleItemTag(r); + break; + case WebXmlTag::argument: + handleArgumentTag(r); + break; + case WebXmlTag::teletype: + handleArgumentTag(r); + break; + case WebXmlTag::link: + handleLinkTag(r); + break; + case WebXmlTag::inlineimage: + handleInlineImageTag(r); + break; + case WebXmlTag::image: + handleImageTag(r); + break; + case WebXmlTag::list: + handleListTag(r); + break; + case WebXmlTag::term: + handleTermTag(r); + break; + case WebXmlTag::raw: + handleRawTag(r); + break; + case WebXmlTag::underline: + handleItalicTag(r); + break; + case WebXmlTag::superscript: + handleSuperScriptTag(r); + break; + case WebXmlTag::code: + case WebXmlTag::badcode: + case WebXmlTag::legalese: + handleCodeTag(r); + break; + case WebXmlTag::rst: + handleRstPassTroughTag(r); + break; + case WebXmlTag::section: + handleAnchorTag(r); + break; + case WebXmlTag::quotefile: + handleQuoteFileTag(r); + break; + case WebXmlTag::generatedlist: + case WebXmlTag::tableofcontents: + case WebXmlTag::quotefromfile: + case WebXmlTag::skipto: + handleIgnoredTag(r); + break; + case WebXmlTag::target: + handleTargetTag(r); + break; + case WebXmlTag::page: + case WebXmlTag::group: + handlePageTag(r); + break; + case WebXmlTag::description: + case WebXmlTag::definition: + case WebXmlTag::printuntil: + case WebXmlTag::relation: + handleUselessTag(r); + break; + case WebXmlTag::title: + handleHeadingTag(r); + break; + case WebXmlTag::ref: + case WebXmlTag::computeroutput: + case WebXmlTag::detaileddescription: + case WebXmlTag::name: + handleParaTag(r); + break; + case WebXmlTag::listitem: + case WebXmlTag::parametername: + case WebXmlTag::parameteritem: + handleItemTag(r); + break; + case WebXmlTag::ulink: + handleLinkTag(r); + break; + case WebXmlTag::itemizedlist: + case WebXmlTag::parameternamelist: + case WebXmlTag::parameterlist: + handleListTag(r); + break; + case WebXmlTag::highlight: + case WebXmlTag::linebreak: + case WebXmlTag::programlisting: + case WebXmlTag::xreftitle: + case WebXmlTag::sp: + case WebXmlTag::entry: + case WebXmlTag::simplesect: + case WebXmlTag::verbatim: + case WebXmlTag::xrefsect: + case WebXmlTag::xrefdescription: + handleIgnoredTag(r); + break; + case WebXmlTag::Unknown: + break; + } +} + +void QtXmlToSphinx::formatCurrentTable() +{ + Q_ASSERT(!m_tables.isEmpty()); + auto &table = m_tables.back(); + if (table.isEmpty()) + return; + table.normalize(); + m_output << '\n'; + table.format(m_output); +} + +void QtXmlToSphinx::pushOutputBuffer() +{ + 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()); + m_buffers.pop(); + 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()); + if (doc.trimmed().isEmpty()) + return doc; + + pushOutputBuffer(); + + QXmlStreamReader reader(doc); + + m_output << autoTranslatedPlaceholder; + Indentation indentation(m_output); + + while (!reader.atEnd()) { + QXmlStreamReader::TokenType token = reader.readNext(); + if (reader.hasError()) { + QString message; + QTextStream(&message) << "XML Error " + << reader.errorString() << " at " << reader.lineNumber() + << ':' << reader.columnNumber() << '\n' << doc; + m_output << message; + throw Exception(message); + break; + } + + if (token == QXmlStreamReader::StartElement) { + WebXmlTag tag = webXmlTagHash().value(reader.name(), WebXmlTag::Unknown); + if (!m_tagStack.isEmpty() && tag == WebXmlTag::raw) + tag = WebXmlTag::Unknown; + m_tagStack.push(tag); + } + + if (!m_tagStack.isEmpty()) + callHandler(m_tagStack.top(), reader); + + if (token == QXmlStreamReader::EndElement) { + m_tagStack.pop(); + m_lastTagName = reader.name().toString(); + } + } + + if (!m_inlineImages.isEmpty()) { + // Write out inline image definitions stored in handleInlineImageTag(). + m_output << '\n' << disableIndent; + for (const InlineImage &img : std::as_const(m_inlineImages)) + m_output << ".. |" << img.tag << "| image:: " << img.href << '\n'; + m_output << '\n' << enableIndent; + m_inlineImages.clear(); + } + + 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(u'/'); + location.append(path); + if (QFileInfo::exists(location)) + return location; + } + return QString(); +} + +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; + 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}; + } + + *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; + } + } + + if (code.isEmpty()) + *errorMessage = msgEmptySnippet(inputFile, lineNo, identifier); + + return code; +} + +QString QtXmlToSphinx::readFromLocation(const QString &location, const QString &identifier, + QString *errorMessage) +{ + QFile inputFile; + inputFile.setFileName(location); + if (!inputFile.open(QIODevice::ReadOnly)) { + QTextStream(errorMessage) << "Could not read code snippet file: " + << QDir::toNativeSeparators(inputFile.fileName()) + << ": " << inputFile.errorString(); + return {}; // null + } + + QString code = u""_s; // non-null + if (identifier.isEmpty()) { + while (!inputFile.atEnd()) + code += QString::fromUtf8(inputFile.readLine()); + return CodeSnipHelpers::fixSpaces(code); + } + + code = readSnippet(inputFile, identifier, errorMessage); + return code.isEmpty() ? QString{} : CodeSnipHelpers::fixSpaces(code); // maintain isNull() +} + +void QtXmlToSphinx::handleHeadingTag(QXmlStreamReader& reader) +{ + static int headingSize = 0; + static char type; + static char types[] = { '-', '^' }; + QXmlStreamReader::TokenType token = reader.tokenType(); + if (token == QXmlStreamReader::StartElement) { + uint typeIdx = reader.attributes().value(u"level"_s).toUInt(); + if (typeIdx >= sizeof(types)) + type = types[sizeof(types)-1]; + else + type = types[typeIdx]; + } else if (token == QXmlStreamReader::EndElement) { + m_output << disableIndent << Pad(type, headingSize) << "\n\n" + << enableIndent; + } else if (token == QXmlStreamReader::Characters) { + m_output << "\n\n" << disableIndent; + headingSize = writeEscapedRstText(m_output, reader.text().trimmed()); + m_output << '\n' << enableIndent; + } +} + +void QtXmlToSphinx::handleParaTag(QXmlStreamReader& reader) +{ + switch (reader.tokenType()) { + case QXmlStreamReader::StartElement: + handleParaTagStart(); + break; + case QXmlStreamReader::EndElement: + handleParaTagEnd(); + break; + case QXmlStreamReader::Characters: + handleParaTagText(reader); + break; + default: + break; + } +} + +void QtXmlToSphinx::handleParaTagStart() +{ + pushOutputBuffer(); +} + +void QtXmlToSphinx::handleParaTagText(QXmlStreamReader& reader) +{ + const auto text = reader.text(); + const QChar end = m_output.lastChar(); + if (!text.isEmpty() && m_output.indentation() == 0 && !end.isNull()) { + QChar start = text[0]; + if ((end == u'*' || end == u'`') && start != u' ' && !start.isPunct()) + m_output << '\\'; + } + m_output << escape(text); +} + +void QtXmlToSphinx::handleParaTagEnd() +{ + QString result = popOutputBuffer().simplified(); + if (result.startsWith(u"**Warning:**")) + result.replace(0, 12, ".. warning:: "_L1); + else if (result.startsWith(u"**Note:**")) + result.replace(0, 9, ".. note:: "_L1); + m_output << result << "\n\n"; +} + +void QtXmlToSphinx::handleItalicTag(QXmlStreamReader& reader) +{ + 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) +{ + 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) +{ + 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; + } +} + +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 == 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 u"raw"_s; + return type == u"enum" || type == u"page" + ? type : u"href"_s; +} + +// "See also" links may appear as nested links: +// <see-also>QAbstractXmlReceiver<link raw="isValid()" href="qxmlquery.html#isValid" type="function">isValid()</link> +// which is handled in handleLinkTag +// or direct text: +// <see-also>rootIsDecorated()</see-also> +// which is handled here. + +void QtXmlToSphinx::handleSeeAlsoTag(QXmlStreamReader& reader) +{ + switch (reader.tokenType()) { + case QXmlStreamReader::StartElement: + m_output << ".. seealso:: "; + break; + case QXmlStreamReader::Characters: { + // Direct embedded link: <see-also>rootIsDecorated()</see-also> + const auto textR = reader.text().trimmed(); + if (!textR.isEmpty()) { + const QString text = textR.toString(); + if (m_seeAlsoContext.isNull()) { + const QString type = text.endsWith(u"()") + ? functionLinkType : classLinkType; + m_seeAlsoContext.reset(handleLinkStart(type, text)); + } + handleLinkText(m_seeAlsoContext.data(), text); + } + } + break; + case QXmlStreamReader::EndElement: + if (!m_seeAlsoContext.isNull()) { // direct, no nested </link> seen + handleLinkEnd(m_seeAlsoContext.data()); + m_seeAlsoContext.reset(); + } + m_output << "\n\n"; + break; + default: + break; + } +} + +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(u'\n'); + for (const auto &line : lines) { + if (!line.trimmed().isEmpty()) + str << indent << line; + str << '\n'; + } +} + +static QString msgSnippetComparison(const QString &location, const QString &identifier, + const QString &pythonCode, const QString &fallbackCode) +{ + StringStream str; + str.setTabWidth(2); + str << "Python snippet " << location; + if (!identifier.isEmpty()) + str << " [" << identifier << ']'; + str << ":\n" << indent << pythonCode << ensureEndl << outdent + << "Corresponding fallback snippet:\n" + << indent << fallbackCode << ensureEndl << outdent << "-- end --\n"; + return str; +} + +void QtXmlToSphinx::handleSnippetTag(QXmlStreamReader& reader) +{ + QXmlStreamReader::TokenType token = reader.tokenType(); + if (token == QXmlStreamReader::StartElement) { + const bool consecutiveSnippet = m_lastTagName == u"snippet" + || m_lastTagName == u"dots" || m_lastTagName == u"codeline"; + if (consecutiveSnippet) { + m_output.flush(); + m_output.string()->chop(1); // Strip newline from previous snippet + } + 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 Snippet snippet = readSnippetFromLocations(location, identifier, + fallbackPath, &errorMessage); + if (!errorMessage.isEmpty()) + warn(msgTagWarning(reader, m_context, m_lastTagName, errorMessage)); + + 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); + if (snippet.result == Snippet::Error) + m_output << "<Code snippet \"" << location << ':' << identifier << "\" not found>\n"; + else + 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 == u"snippet" + || m_lastTagName == u"dots" || m_lastTagName == u"codeline"; + if (consecutiveSnippet) { + m_output.flush(); + m_output.string()->chop(2); + } else { + m_output << "::\n\n"; + } + pushOutputBuffer(); + int indent = reader.attributes().value(u"indent"_s).toInt() + + m_output.indentation() * m_output.tabWidth(); + for (int i = 0; i < indent; ++i) + m_output << ' '; + } else if (token == QXmlStreamReader::Characters) { + m_output << reader.text().toString().trimmed(); + } else if (token == QXmlStreamReader::EndElement) { + m_output << disableIndent << popOutputBuffer() << "\n\n\n" << enableIndent; + } +} + +void QtXmlToSphinx::handleTableTag(QXmlStreamReader& reader) +{ + QXmlStreamReader::TokenType token = reader.tokenType(); + if (token == QXmlStreamReader::StartElement) { + if (parentTag() == WebXmlTag::para) + handleParaTagEnd(); // End <para> to prevent the table from being rst-escaped + m_tables.push({}); + } else if (token == QXmlStreamReader::EndElement) { + // write the table on m_output + formatCurrentTable(); + m_tables.pop(); + if (parentTag() == WebXmlTag::para) + handleParaTagStart(); + } +} + +void QtXmlToSphinx::handleTermTag(QXmlStreamReader& reader) +{ + QXmlStreamReader::TokenType token = reader.tokenType(); + if (token == QXmlStreamReader::StartElement) { + pushOutputBuffer(); + } else if (token == QXmlStreamReader::Characters) { + m_output << reader.text().toString().replace(u"::"_s, u"."_s); + } else if (token == QXmlStreamReader::EndElement) { + TableCell cell; + cell.data = popOutputBuffer().trimmed(); + m_tables.back().appendRow(TableRow(1, cell)); + } +} + + +void QtXmlToSphinx::handleItemTag(QXmlStreamReader& reader) +{ + QXmlStreamReader::TokenType token = reader.tokenType(); + if (token == QXmlStreamReader::StartElement) { + auto &table = m_tables.back(); + if (table.isEmpty()) + table.appendRow({}); + TableRow& row = table.last(); + TableCell cell; + 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 = trimLeadingNewlines(trimRight(popOutputBuffer())); + auto &table = m_tables.back(); + if (!table.isEmpty()) { + TableRow& row = table.last(); + if (!row.isEmpty()) + row.last().data = data; + } + } +} + +void QtXmlToSphinx::handleHeaderTag(QXmlStreamReader &reader) +{ + // <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 == u"enum") + return EnumeratedList; + if (t == u"ordered") + return OrderedList; + return BulletList; +} + +void QtXmlToSphinx::handleListTag(QXmlStreamReader& reader) +{ + static ListType listType = BulletList; + QXmlStreamReader::TokenType token = reader.tokenType(); + if (token == QXmlStreamReader::StartElement) { + m_tables.push({}); + auto &table = m_tables.back(); + listType = webXmlListType(reader.attributes().value(u"type"_s)); + if (listType == EnumeratedList) { + 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(); + 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 : table.constFirst()) { + const auto itemLines = QStringView{cell.data}.split(u'\n'); + m_output << separator << itemLines.constFirst() << '\n'; + for (qsizetype i = 1, max = itemLines.size(); i < max; ++i) + m_output << indentLine << itemLines[i] << '\n'; + } + m_output << '\n'; + } + break; + case EnumeratedList: + formatCurrentTable(); + break; + } + } + m_tables.pop(); + } +} + +void QtXmlToSphinx::handleLinkTag(QXmlStreamReader& reader) +{ + switch (reader.tokenType()) { + 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(u"type"_s)); + const QString ref = reader.attributes().value(linkSourceAttribute(type)).toString(); + m_linkContext.reset(handleLinkStart(type, ref)); + } + break; + case QXmlStreamReader::Characters: + Q_ASSERT(!m_linkContext.isNull()); + handleLinkText(m_linkContext.data(), reader.text().toString()); + break; + case QXmlStreamReader::EndElement: + Q_ASSERT(!m_linkContext.isNull()); + handleLinkEnd(m_linkContext.data()); + m_linkContext.reset(); + break; + default: + break; + } +} + +QtXmlToSphinxLink *QtXmlToSphinx::handleLinkStart(const QString &type, QString ref) const +{ + ref.replace(u"::"_s, u"."_s); + ref.remove(u"()"_s); + auto *result = new QtXmlToSphinxLink(ref); + + if (m_insideBold) + result->flags |= QtXmlToSphinxLink::InsideBold; + else if (m_insideItalic) + result->flags |= QtXmlToSphinxLink::InsideItalic; + + if (type == u"external" || isHttpLink(ref)) { + result->type = QtXmlToSphinxLink::External; + } else if (type == functionLinkType && !m_context.isEmpty()) { + result->type = QtXmlToSphinxLink::Method; + 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 + u'.'); + } else { + result->linkRef = m_generator->expandFunction(result->linkRef); + } + } else if (type == functionLinkType && m_context.isEmpty()) { + result->type = QtXmlToSphinxLink::Function; + } else if (type == classLinkType) { + result->type = QtXmlToSphinxLink::Class; + result->linkRef = m_generator->expandClass(m_context, result->linkRef); + } else if (type == u"enum") { + result->type = QtXmlToSphinxLink::Attribute; + } else if (type == u"page") { + // Module, external web page or reference + if (result->linkRef == m_parameters.moduleName) + result->type = QtXmlToSphinxLink::Module; + else + result->type = QtXmlToSphinxLink::Reference; + } else { + result->type = QtXmlToSphinxLink::Reference; + } + return result; +} + +// <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="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> +// <link raw="QNetworkSession::reject()" href="qnetworksession.html#reject" type="function">QNetworkSession::reject()</link> + +static QString fixLinkText(const QtXmlToSphinxLink *linkContext, + QString linktext) +{ + if (linkContext->type == QtXmlToSphinxLink::External + || linkContext->type == QtXmlToSphinxLink::Reference) { + return linktext; + } + // 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(u"::"); + if (lastSep != -1) + linktext.remove(0, lastSep + 2); + else + QtXmlToSphinx::stripPythonQualifiers(&linktext); + if (linkContext->linkRef == linktext) + return {}; + if ((linkContext->type & QtXmlToSphinxLink::FunctionMask) != 0 + && (linkContext->linkRef + u"()"_s) == linktext) { + return {}; + } + return linktext; +} + +void QtXmlToSphinx::handleLinkText(QtXmlToSphinxLink *linkContext, const QString &linktext) +{ + linkContext->linkText = fixLinkText(linkContext, linktext); +} + +void QtXmlToSphinx::handleLinkEnd(QtXmlToSphinxLink *linkContext) +{ + m_output << m_generator->resolveLink(*linkContext); +} + +WebXmlTag QtXmlToSphinx::parentTag() const +{ + const auto index = m_tagStack.size() - 2; + return index >= 0 ? m_tagStack.at(index) : WebXmlTag::Unknown; +} + +// Copy images that are placed in a subdirectory "images" under the webxml files +// by qdoc to a matching subdirectory under the "rst/PySide6/<module>" directory +static bool copyImage(const QString &docDataDir, const QString &relativeSourceFile, + const QString &outputDir, const QString &relativeTargetFile, + const QLoggingCategory &lc, QString *errorMessage) +{ + QString targetFileName = outputDir + u'/' + relativeTargetFile; + if (QFileInfo::exists(targetFileName)) + return true; + + QString relativeTargetDir = relativeTargetFile; + relativeTargetDir.truncate(qMax(relativeTargetDir.lastIndexOf(u'/'), qsizetype(0))); + if (!relativeTargetDir.isEmpty() && !QFileInfo::exists(outputDir + u'/' + relativeTargetDir)) { + const QDir outDir(outputDir); + if (!outDir.mkpath(relativeTargetDir)) { + QTextStream(errorMessage) << "Cannot create " << QDir::toNativeSeparators(relativeTargetDir) + << " under " << QDir::toNativeSeparators(outputDir); + return false; + } + } + + QFile source(docDataDir + u'/' + relativeSourceFile); + if (!source.copy(targetFileName)) { + QTextStream(errorMessage) << "Cannot copy " << QDir::toNativeSeparators(source.fileName()) + << " to " << QDir::toNativeSeparators(targetFileName) << ": " + << source.errorString(); + return false; + } + + qCDebug(lc).noquote().nospace() << __FUNCTION__ << " \"" << relativeSourceFile + << "\"->\"" << relativeTargetFile << '"'; + return true; +} + +bool QtXmlToSphinx::copyImage(const QString &href) const +{ + QString errorMessage; + const auto imagePaths = m_generator->resolveImage(href, m_context); + const bool result = ::copyImage(m_parameters.docDataDir, + imagePaths.source, + m_parameters.outputDirectory, + imagePaths.target, + m_generator->loggingCategory(), + &errorMessage); + if (!result) + throw Exception(errorMessage); + return result; +} + +void QtXmlToSphinx::handleImageTag(QXmlStreamReader& reader) +{ + if (reader.tokenType() != QXmlStreamReader::StartElement) + return; + const QString href = reader.attributes().value(u"href"_s).toString(); + if (copyImage(href)) + m_output << ".. image:: " << href << "\n\n"; +} + +void QtXmlToSphinx::handleInlineImageTag(QXmlStreamReader& reader) +{ + if (reader.tokenType() != QXmlStreamReader::StartElement) + return; + 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; + auto pos = tag.lastIndexOf(u'/'); + if (pos != -1) + tag.remove(0, pos + 1); + pos = tag.indexOf(u'.'); + if (pos != -1) + tag.truncate(pos); + tag += QString::number(m_inlineImages.size() + 1); + m_inlineImages.append(InlineImage{tag, href}); + m_output << '|' << tag << '|' << ' '; +} + +void QtXmlToSphinx::handleRawTag(QXmlStreamReader& reader) +{ + QXmlStreamReader::TokenType token = reader.tokenType(); + if (token == QXmlStreamReader::StartElement) { + 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); + m_output << reader.text(); + } else if (token == QXmlStreamReader::EndElement) { + m_output << "\n\n"; + } +} + +void QtXmlToSphinx::handleCodeTag(QXmlStreamReader& reader) +{ + QXmlStreamReader::TokenType token = reader.tokenType(); + if (token == QXmlStreamReader::StartElement) { + m_output << "::\n\n" << indent; + } else if (token == QXmlStreamReader::Characters) { + Indentation indent(m_output); + m_output << reader.text(); + } else if (token == QXmlStreamReader::EndElement) { + m_output << outdent << "\n\n"; + } +} + +void QtXmlToSphinx::handleUnknownTag(QXmlStreamReader& reader) +{ + QXmlStreamReader::TokenType token = reader.tokenType(); + if (token == QXmlStreamReader::StartElement) { + qCDebug(m_generator->loggingCategory()).noquote().nospace() + << "Unknown QtDoc tag: \"" << reader.name().toString() << "\"."; + } +} + +void QtXmlToSphinx::handleSuperScriptTag(QXmlStreamReader& reader) +{ + QXmlStreamReader::TokenType token = reader.tokenType(); + if (token == QXmlStreamReader::StartElement) { + m_output << " :sup:`"; + pushOutputBuffer(); + } else if (token == QXmlStreamReader::Characters) { + m_output << reader.text().toString(); + } else if (token == QXmlStreamReader::EndElement) { + m_output << popOutputBuffer(); + m_output << '`'; + } +} + +void QtXmlToSphinx::handlePageTag(QXmlStreamReader &reader) +{ + if (reader.tokenType() != QXmlStreamReader::StartElement) + return; + + m_output << disableIndent; + + const auto title = reader.attributes().value("title"); + if (!title.isEmpty()) + m_output << rstLabel(title.toString()); + + const auto fullTitle = reader.attributes().value("fulltitle"); + const int size = fullTitle.isEmpty() + ? writeEscapedRstText(m_output, title) + : writeEscapedRstText(m_output, fullTitle); + + m_output << '\n' << Pad('*', size) << "\n\n" + << enableIndent; +} + +void QtXmlToSphinx::handleTargetTag(QXmlStreamReader &reader) +{ + if (reader.tokenType() != QXmlStreamReader::StartElement) + return; + const auto name = reader.attributes().value("name"); + if (!name.isEmpty()) + m_output << rstLabel(name.toString()); +} + +void QtXmlToSphinx::handleIgnoredTag(QXmlStreamReader&) +{ +} + +void QtXmlToSphinx::handleUselessTag(QXmlStreamReader&) +{ + // Tag "description" just marks the init of "Detailed description" title. + // Tag "definition" just marks enums. We have a different way to process them. +} + +void QtXmlToSphinx::handleAnchorTag(QXmlStreamReader& reader) +{ + QXmlStreamReader::TokenType token = reader.tokenType(); + if (token == QXmlStreamReader::StartElement) { + QString anchor; + 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 + u'_'); + m_output << rstLabel(anchor); + } + } else if (token == QXmlStreamReader::EndElement) { + m_opened_anchor.clear(); + } +} + +void QtXmlToSphinx::handleRstPassTroughTag(QXmlStreamReader& reader) +{ + if (reader.tokenType() == QXmlStreamReader::Characters) + m_output << reader.text(); +} + +void QtXmlToSphinx::handleQuoteFileTag(QXmlStreamReader& reader) +{ + QXmlStreamReader::TokenType token = reader.tokenType(); + if (token == QXmlStreamReader::Characters) { + QString location = reader.text().toString(); + location.prepend(m_parameters.libSourceDir + u'/'); + QString errorMessage; + QString code = readFromLocation(location, QString(), &errorMessage); + if (!errorMessage.isEmpty()) + warn(msgTagWarning(reader, m_context, m_lastTagName, errorMessage)); + m_output << "::\n\n"; + Indentation indentation(m_output); + if (code.isEmpty()) + m_output << "<Code snippet \"" << location << "\" not found>\n"; + else + m_output << code << ensureEndl; + m_output << '\n'; + } +} + +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) + 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. + 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 (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) + m_rows[row].insert(col + 1, newCell); + cell.colSpan = 0; + col++; + } else if (mergeCols) { + m_rows[row][maxCols - 1].data += u' ' + cell.data; + } + } + } + + // row spans + 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; + 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); + row++; + } + } + } + } + m_normalized = true; +} + +void QtXmlToSphinx::Table::format(TextStream& s) const +{ + if (isEmpty()) + return; + + Q_ASSERT(isNormalized()); + + // calc width and height of each column and row + 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 (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], str.size()); + rowHeights[i] = std::max(rowHeights[i], rowLines.size()); + } + } + + if (!*std::max_element(colWidths.begin(), colWidths.end())) + return; // empty table (table with empty cells) + + // create a horizontal line to be used later. + QString horizontalLine = u"+"_s; + for (auto colWidth : colWidths) + horizontalLine += QString(colWidth, u'-') + u'+'; + + // write table rows + 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 (qsizetype col = 0; col < headerColumnCount; ++col) { + char c = '-'; + if (col >= row.size() || row[col].rowSpan == -1) + c = ' '; + else if (i == 1 && hasHeader()) + c = '='; + s << Pad(c, colWidths.at(col)) << '+'; + } + s << '\n'; + + + // Print the table cells + 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]; + // FIXME: Cache this!!! + const auto rowLines = QStringView{cell.data}.split(u'\n'); + + if (!j || !cell.colSpan) + s << '|'; + else + s << ' '; + const auto width = int(colWidths.at(j)); + if (rowLine < rowLines.size()) + s << AlignedField(rowLines.at(rowLine), width); + else + s << Pad(' ', width); + } + for ( ; j < headerColumnCount; ++j) // pad + s << '|' << Pad(' ', colWidths.at(j)); + s << "|\n"; + } + } + 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(u'.'); + if (lastSep != -1) + s->remove(0, lastSep + 1); +} + +void QtXmlToSphinx::warn(const QString &message) const +{ + qCWarning(m_generator->loggingCategory(), "%s", qPrintable(message)); +} + +void QtXmlToSphinx::debug(const QString &message) const +{ + qCDebug(m_generator->loggingCategory(), "%s", qPrintable(message)); +} diff --git a/sources/shiboken6/generator/qtdoc/qtxmltosphinx.h b/sources/shiboken6/generator/qtdoc/qtxmltosphinx.h new file mode 100644 index 000000000..398c5bc97 --- /dev/null +++ b/sources/shiboken6/generator/qtdoc/qtxmltosphinx.h @@ -0,0 +1,216 @@ +// 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/QList> +#include <QtCore/QScopedPointer> +#include <QtCore/QStack> + +#include <memory> + +QT_BEGIN_NAMESPACE +class QDebug; +class QXmlStreamReader; +QT_END_NAMESPACE + +class QtXmlToSphinxDocGeneratorInterface; +struct QtXmlToSphinxParameters; +struct QtXmlToSphinxLink; + +enum class WebXmlTag; + +class QtXmlToSphinx +{ +public: + Q_DISABLE_COPY_MOVE(QtXmlToSphinx) + + struct InlineImage + { + QString tag; + QString href; + }; + + struct TableCell + { + short rowSpan = 0; + short colSpan = 0; + QString data; + + TableCell(const QString& text = QString()) : data(text) {} + TableCell(const char* text) : data(QString::fromLatin1(text)) {} + }; + + using TableRow = QList<TableCell>; + + class Table + { + public: + Table() = default; + + bool isEmpty() const { return m_rows.isEmpty(); } + + void setHeaderEnabled(bool enable) + { + m_hasHeader = enable; + } + + bool hasHeader() const + { + return m_hasHeader; + } + + void normalize(); + + bool isNormalized() const + { + return m_normalized; + } + + void appendRow(const TableRow &row) { m_rows.append(row); } + + 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; + }; + + explicit QtXmlToSphinx(const QtXmlToSphinxDocGeneratorInterface *docGenerator, + const QtXmlToSphinxParameters ¶meters, + const QString& doc, + const QString& context = QString()); + ~QtXmlToSphinx(); + + QString result() const + { + return m_result; + } + + static void stripPythonQualifiers(QString *s); + + // For testing + static QString readSnippet(QIODevice &inputFile, const QString &identifier, + QString *errorMessage); + +private: + using StringSharedPtr = std::shared_ptr<QString>; + + QString transform(const QString& doc); + + void handleHeadingTag(QXmlStreamReader& reader); + void handleParaTag(QXmlStreamReader& reader); + void handleParaTagStart(); + void handleParaTagText(QXmlStreamReader &reader); + void handleParaTagEnd(); + void handleItalicTag(QXmlStreamReader& reader); + void handleBoldTag(QXmlStreamReader& reader); + void handleArgumentTag(QXmlStreamReader& reader); + void handleSeeAlsoTag(QXmlStreamReader& reader); + void handleSnippetTag(QXmlStreamReader& reader); + void handleDotsTag(QXmlStreamReader& reader); + void handleLinkTag(QXmlStreamReader& reader); + void handleImageTag(QXmlStreamReader& reader); + void handleInlineImageTag(QXmlStreamReader& reader); + void handleListTag(QXmlStreamReader& reader); + void handleTermTag(QXmlStreamReader& reader); + void handleSuperScriptTag(QXmlStreamReader& reader); + void handleQuoteFileTag(QXmlStreamReader& reader); + + // 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); + void handleCodeTag(QXmlStreamReader& reader); + void handlePageTag(QXmlStreamReader&); + void handleTargetTag(QXmlStreamReader&); + + void handleIgnoredTag(QXmlStreamReader& reader); + void handleUnknownTag(QXmlStreamReader& reader); + void handleUselessTag(QXmlStreamReader& reader); + void handleAnchorTag(QXmlStreamReader& reader); + void handleRstPassTroughTag(QXmlStreamReader& reader); + + QtXmlToSphinxLink *handleLinkStart(const QString &type, QString ref) const; + static void handleLinkText(QtXmlToSphinxLink *linkContext, const QString &linktext) ; + void handleLinkEnd(QtXmlToSphinxLink *linkContext); + WebXmlTag parentTag() const; + + void warn(const QString &message) const; + void debug(const QString &message) const; + + QStack<WebXmlTag> m_tagStack; + TextStream m_output; + QString m_result; + + QStack<StringSharedPtr> m_buffers; // Maintain address stability since it used in TextStream + + 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> + 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; + + 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(); + QString popOutputBuffer(); + void writeTable(Table& table); + bool copyImage(const QString &href) const; + void callHandler(WebXmlTag t, QXmlStreamReader &); + void formatCurrentTable(); +}; + +inline TextStream& operator<<(TextStream& s, const QtXmlToSphinx& xmlToSphinx) +{ + return s << xmlToSphinx.result(); +} + +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 new file mode 100644 index 000000000..d4a098a12 --- /dev/null +++ b/sources/shiboken6/generator/qtdoc/qtxmltosphinxinterface.h @@ -0,0 +1,68 @@ +// 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 + +#include <QtCore/QStringList> + +QT_FORWARD_DECLARE_CLASS(QLoggingCategory) + +struct QtXmlToSphinxParameters +{ + QString moduleName; + QString docDataDir; + QString outputDirectory; + QString libSourceDir; + QStringList codeSnippetDirs; + QString codeSnippetRewriteOld; + QString codeSnippetRewriteNew; + bool snippetComparison = false; +}; + +struct QtXmlToSphinxLink +{ + enum Type + { + Method = 0x1, Function = 0x2, + FunctionMask = Method | Function, + Class = 0x4, Attribute = 0x8, Module = 0x10, + Reference = 0x20, External= 0x40 + }; + + enum Flags { InsideBold = 0x1, InsideItalic = 0x2 }; + + explicit QtXmlToSphinxLink(const QString &ref) : linkRef(ref) {} + + QString linkRef; + QString linkText; + Type type = Reference; + int flags = 0; +}; + +class QtXmlToSphinxDocGeneratorInterface +{ +public: + virtual QString expandFunction(const QString &function) const = 0; + virtual QString expandClass(const QString &context, + const QString &name) const = 0; + virtual QString resolveContextForMethod(const QString &context, + const QString &methodName) const = 0; + + virtual const QLoggingCategory &loggingCategory() const = 0; + + virtual QtXmlToSphinxLink resolveLink(const QtXmlToSphinxLink &) const = 0; + + // Resolve images paths relative to doc data directory/output directory. + struct Image + { + QString source; + QString target; + }; + + virtual Image resolveImage(const QString &href, const QString &context) const = 0; + + virtual ~QtXmlToSphinxDocGeneratorInterface() = default; +}; + +#endif // QTXMLTOSPHINXINTERFACE_H diff --git a/sources/shiboken6/generator/qtdoc/rstformat.h b/sources/shiboken6/generator/qtdoc/rstformat.h new file mode 100644 index 000000000..8af7671fb --- /dev/null +++ b/sources/shiboken6/generator/qtdoc/rstformat.h @@ -0,0 +1,99 @@ +// 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 + +#include <textstream.h> + +#include <QtCore/QByteArray> +#include <QtCore/QString> +#include <QtCore/QTextStream> +#include <QtCore/QVersionNumber> + +struct rstVersionAdded +{ + explicit rstVersionAdded(const QVersionNumber &v) : m_version(v) {} + + const QVersionNumber m_version; +}; + +inline TextStream &operator<<(TextStream &s, const rstVersionAdded &v) +{ + s << ".. versionadded:: "<< v.m_version.toString() << "\n\n"; + return s; +} + +inline QByteArray rstDeprecationNote(const char *what) +{ + return QByteArrayLiteral(".. note:: This ") + + what + QByteArrayLiteral(" is deprecated.\n\n"); +} + +template <class String> +inline int writeEscapedRstText(TextStream &str, const String &s) +{ + int escaped = 0; + for (const QChar &c : s) { + switch (c.unicode()) { + case '*': + case '`': + case '_': + case '\\': + str << '\\'; + ++escaped; + break; + } + str << c; + } + return s.size() + escaped; +} + +class escape +{ +public: + explicit escape(QStringView s) : m_string(s) {} + + void write(TextStream &str) const { writeEscapedRstText(str, m_string); } + +private: + const QStringView m_string; +}; + +inline TextStream &operator<<(TextStream &str, const escape &e) +{ + e.write(str); + return str; +} + +// RST anchor string: Anything else but letters, numbers, '_' or '.' replaced by '-' +inline bool isValidRstLabelChar(QChar c) +{ + 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] = u'-'; + } + return s; +} + +class rstLabel +{ +public: + explicit rstLabel(const QString &l) : m_label(l) {} + + friend TextStream &operator<<(TextStream &str, const rstLabel &a) + { + str << ".. _" << toRstLabel(a.m_label) << ":\n\n"; + return str; + } + +private: + const QString &m_label; +}; + +#endif // RSTFORMAT_H |