diff options
Diffstat (limited to 'sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp')
-rw-r--r-- | sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp | 274 |
1 files changed, 184 insertions, 90 deletions
diff --git a/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp b/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp index c29d2596c..55c1d2090 100644 --- a/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp +++ b/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp @@ -12,16 +12,13 @@ #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; -static inline QString nameAttribute() { return QStringLiteral("name"); } -static inline QString titleAttribute() { return QStringLiteral("title"); } -static inline QString fullTitleAttribute() { return QStringLiteral("fulltitle"); } - QString msgTagWarning(const QXmlStreamReader &reader, const QString &context, const QString &tag, const QString &message) { @@ -62,6 +59,20 @@ static bool isHttpLink(const QString &ref) return ref.startsWith(u"http://") || ref.startsWith(u"https://"); } +static QString trimRight(QString s) +{ + while (!s.isEmpty() && s.crbegin()->isSpace()) + s.chop(1); + return s; +} + +static QString trimLeadingNewlines(QString s) +{ + while (!s.isEmpty() && s.at(0) == u'\n') + s.remove(0, 1); + return s; +} + QDebug operator<<(QDebug d, const QtXmlToSphinxLink &l) { static const QHash<QtXmlToSphinxLink::Type, const char *> typeName = { @@ -406,32 +417,49 @@ void QtXmlToSphinx::callHandler(WebXmlTag t, QXmlStreamReader &r) void QtXmlToSphinx::formatCurrentTable() { - if (m_currentTable.isEmpty()) + Q_ASSERT(!m_tables.isEmpty()); + auto &table = m_tables.back(); + if (table.isEmpty()) return; - m_currentTable.normalize(); + table.normalize(); m_output << '\n'; - m_currentTable.format(m_output); + table.format(m_output); } void QtXmlToSphinx::pushOutputBuffer() { - m_buffers.append(StringSharedPtr(new QString{})); - m_output.setString(m_buffers.top().data()); + m_buffers.append(std::make_shared<QString>()); + m_output.setString(m_buffers.top().get()); } QString QtXmlToSphinx::popOutputBuffer() { Q_ASSERT(!m_buffers.isEmpty()); - QString result(*m_buffers.top().data()); + QString result(*m_buffers.top()); m_buffers.pop(); - m_output.setString(m_buffers.isEmpty() ? nullptr : m_buffers.top().data()); + m_output.setString(m_buffers.isEmpty() ? nullptr : m_buffers.top().get()); return result; } +constexpr auto autoTranslatedPlaceholder = "AUTO_GENERATED\n"_L1; +constexpr auto autoTranslatedNote = +R"(.. warning:: + This section contains snippets that were automatically + translated from C++ to Python and may contain errors. + +)"_L1; + +void QtXmlToSphinx::setAutoTranslatedNote(QString *str) const +{ + if (m_containsAutoTranslations) + str->replace(autoTranslatedPlaceholder, autoTranslatedNote); + else + str->remove(autoTranslatedPlaceholder); +} + QString QtXmlToSphinx::transform(const QString& doc) { Q_ASSERT(m_buffers.isEmpty()); - Indentation indentation(m_output); if (doc.trimmed().isEmpty()) return doc; @@ -439,6 +467,9 @@ QString QtXmlToSphinx::transform(const QString& doc) QXmlStreamReader reader(doc); + m_output << autoTranslatedPlaceholder; + Indentation indentation(m_output); + while (!reader.atEnd()) { QXmlStreamReader::TokenType token = reader.readNext(); if (reader.hasError()) { @@ -470,7 +501,7 @@ QString QtXmlToSphinx::transform(const QString& doc) if (!m_inlineImages.isEmpty()) { // Write out inline image definitions stored in handleInlineImageTag(). m_output << '\n' << disableIndent; - for (const InlineImage &img : qAsConst(m_inlineImages)) + for (const InlineImage &img : std::as_const(m_inlineImages)) m_output << ".. |" << img.tag << "| image:: " << img.href << '\n'; m_output << '\n' << enableIndent; m_inlineImages.clear(); @@ -479,6 +510,7 @@ QString QtXmlToSphinx::transform(const QString& doc) m_output.flush(); QString retval = popOutputBuffer(); Q_ASSERT(m_buffers.isEmpty()); + setAutoTranslatedNote(&retval); return retval; } @@ -527,7 +559,7 @@ static QString pySnippetName(const QString &path, SnippetType type) QtXmlToSphinx::Snippet QtXmlToSphinx::readSnippetFromLocations(const QString &path, const QString &identifier, const QString &fallbackPath, - QString *errorMessage) const + QString *errorMessage) { // For anything else but C++ header/sources (no conversion to Python), // use existing fallback paths first. @@ -549,6 +581,7 @@ QtXmlToSphinx::Snippet QtXmlToSphinx::readSnippetFromLocations(const QString &pa 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}; } } @@ -560,7 +593,7 @@ QtXmlToSphinx::Snippet QtXmlToSphinx::readSnippetFromLocations(const QString &pa } } - resolvedPath =resolveFile(locations, path); + resolvedPath = resolveFile(locations, path); if (!resolvedPath.isEmpty()) { const QString code = readFromLocation(resolvedPath, identifier, errorMessage); return {code, code.isNull() ? Snippet::Error : Snippet::Resolved}; @@ -576,6 +609,88 @@ QtXmlToSphinx::Snippet QtXmlToSphinx::readSnippetFromLocations(const QString &pa 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) { @@ -585,7 +700,7 @@ QString QtXmlToSphinx::readFromLocation(const QString &location, const QString & QTextStream(errorMessage) << "Could not read code snippet file: " << QDir::toNativeSeparators(inputFile.fileName()) << ": " << inputFile.errorString(); - return QString(); // null + return {}; // null } QString code = u""_s; // non-null @@ -595,34 +710,8 @@ QString QtXmlToSphinx::readFromLocation(const QString &location, const QString & return CodeSnipHelpers::fixSpaces(code); } - const QRegularExpression searchString(u"//!\\s*\\["_s - + identifier + u"\\]"_s); - Q_ASSERT(searchString.isValid()); - static const QRegularExpression codeSnippetCode(u"//!\\s*\\[[\\w\\d\\s]+\\]"_s); - Q_ASSERT(codeSnippetCode.isValid()); - - bool getCode = false; - - while (!inputFile.atEnd()) { - QString line = QString::fromUtf8(inputFile.readLine()); - if (getCode && !line.contains(searchString)) { - line.remove(codeSnippetCode); - code += line; - } else if (line.contains(searchString)) { - if (getCode) - break; - getCode = true; - } - } - - if (!getCode) { - QTextStream(errorMessage) << "Code snippet file found (" - << QDir::toNativeSeparators(location) << "), but snippet [" - << identifier << "] not found."; - return QString(); // null - } - - return CodeSnipHelpers::fixSpaces(code); + code = readSnippet(inputFile, identifier, errorMessage); + return code.isEmpty() ? QString{} : CodeSnipHelpers::fixSpaces(code); // maintain isNull() } void QtXmlToSphinx::handleHeadingTag(QXmlStreamReader& reader) @@ -685,9 +774,9 @@ void QtXmlToSphinx::handleParaTagEnd() { QString result = popOutputBuffer().simplified(); if (result.startsWith(u"**Warning:**")) - result.replace(0, 12, QStringLiteral(".. warning:: ")); + result.replace(0, 12, ".. warning:: "_L1); else if (result.startsWith(u"**Note:**")) - result.replace(0, 9, QStringLiteral(".. note:: ")); + result.replace(0, 9, ".. note:: "_L1); m_output << result << "\n\n"; } @@ -756,23 +845,23 @@ void QtXmlToSphinx::handleArgumentTag(QXmlStreamReader& reader) } } -static inline QString functionLinkType() { return QStringLiteral("function"); } -static inline QString classLinkType() { return QStringLiteral("class"); } +constexpr auto functionLinkType = "function"_L1; +constexpr auto classLinkType = "class"_L1; static inline QString fixLinkType(QStringView type) { // TODO: create a flag PROPERTY-AS-FUNCTION to ask if the properties // are recognized as such or not in the binding if (type == u"property") - return functionLinkType(); + return functionLinkType; if (type == u"typedef") - return classLinkType(); + return classLinkType; return type.toString(); } static inline QString linkSourceAttribute(const QString &type) { - if (type == functionLinkType() || type == classLinkType()) + if (type == functionLinkType || type == classLinkType) return u"raw"_s; return type == u"enum" || type == u"page" ? type : u"href"_s; @@ -798,7 +887,7 @@ void QtXmlToSphinx::handleSeeAlsoTag(QXmlStreamReader& reader) const QString text = textR.toString(); if (m_seeAlsoContext.isNull()) { const QString type = text.endsWith(u"()") - ? functionLinkType() : classLinkType(); + ? functionLinkType : classLinkType; m_seeAlsoContext.reset(handleLinkStart(type, text)); } handleLinkText(m_seeAlsoContext.data(), text); @@ -817,7 +906,7 @@ void QtXmlToSphinx::handleSeeAlsoTag(QXmlStreamReader& reader) } } -static inline QString fallbackPathAttribute() { return QStringLiteral("path"); } +constexpr auto fallbackPathAttribute = "path"_L1; template <class Indent> // const char*/class Indentor void formatSnippet(TextStream &str, Indent indent, const QString &snippet) @@ -852,14 +941,15 @@ void QtXmlToSphinx::handleSnippetTag(QXmlStreamReader& reader) || m_lastTagName == u"dots" || m_lastTagName == u"codeline"; if (consecutiveSnippet) { m_output.flush(); - m_output.string()->chop(2); + m_output.string()->chop(1); // Strip newline from previous snippet } QString location = reader.attributes().value(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(); + if (reader.attributes().hasAttribute(fallbackPathAttribute)) + fallbackPath = reader.attributes().value(fallbackPathAttribute).toString(); QString errorMessage; + const Snippet snippet = readSnippetFromLocations(location, identifier, fallbackPath, &errorMessage); if (!errorMessage.isEmpty()) @@ -882,6 +972,7 @@ void QtXmlToSphinx::handleSnippetTag(QXmlStreamReader& reader) m_output << '\n'; } } + void QtXmlToSphinx::handleDotsTag(QXmlStreamReader& reader) { QXmlStreamReader::TokenType token = reader.tokenType(); @@ -912,11 +1003,11 @@ void QtXmlToSphinx::handleTableTag(QXmlStreamReader& reader) if (token == QXmlStreamReader::StartElement) { if (parentTag() == WebXmlTag::para) handleParaTagEnd(); // End <para> to prevent the table from being rst-escaped - m_currentTable.clear(); + m_tables.push({}); } else if (token == QXmlStreamReader::EndElement) { // write the table on m_output formatCurrentTable(); - m_currentTable.clear(); + m_tables.pop(); if (parentTag() == WebXmlTag::para) handleParaTagStart(); } @@ -932,7 +1023,7 @@ void QtXmlToSphinx::handleTermTag(QXmlStreamReader& reader) } else if (token == QXmlStreamReader::EndElement) { TableCell cell; cell.data = popOutputBuffer().trimmed(); - m_currentTable.appendRow(TableRow(1, cell)); + m_tables.back().appendRow(TableRow(1, cell)); } } @@ -941,18 +1032,20 @@ void QtXmlToSphinx::handleItemTag(QXmlStreamReader& reader) { QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { - if (m_currentTable.isEmpty()) - m_currentTable.appendRow({}); - TableRow& row = m_currentTable.last(); + auto &table = m_tables.back(); + if (table.isEmpty()) + table.appendRow({}); + TableRow& row = table.last(); TableCell cell; cell.colSpan = reader.attributes().value(u"colspan"_s).toShort(); cell.rowSpan = reader.attributes().value(u"rowspan"_s).toShort(); row << cell; pushOutputBuffer(); } else if (token == QXmlStreamReader::EndElement) { - QString data = popOutputBuffer().trimmed(); - if (!m_currentTable.isEmpty()) { - TableRow& row = m_currentTable.last(); + QString data = trimLeadingNewlines(trimRight(popOutputBuffer())); + auto &table = m_tables.back(); + if (!table.isEmpty()) { + TableRow& row = table.last(); if (!row.isEmpty()) row.last().data = data; } @@ -965,15 +1058,16 @@ void QtXmlToSphinx::handleHeaderTag(QXmlStreamReader &reader) // C++ header with "name"/"href" attributes. if (reader.tokenType() == QXmlStreamReader::StartElement && !reader.attributes().hasAttribute(u"name"_s)) { - m_currentTable.setHeaderEnabled(true); - m_currentTable.appendRow({}); + auto &table = m_tables.back(); + table.setHeaderEnabled(true); + table.appendRow({}); } } void QtXmlToSphinx::handleRowTag(QXmlStreamReader& reader) { if (reader.tokenType() == QXmlStreamReader::StartElement) - m_currentTable.appendRow({}); + m_tables.back().appendRow({}); } enum ListType { BulletList, OrderedList, EnumeratedList }; @@ -989,27 +1083,29 @@ static inline ListType webXmlListType(QStringView t) void QtXmlToSphinx::handleListTag(QXmlStreamReader& reader) { - // BUG We do not support a list inside a table cell static ListType listType = BulletList; QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { + m_tables.push({}); + auto &table = m_tables.back(); listType = webXmlListType(reader.attributes().value(u"type"_s)); if (listType == EnumeratedList) { - m_currentTable.appendRow(TableRow{TableCell(u"Constant"_s), - TableCell(u"Description"_s)}); - m_currentTable.setHeaderEnabled(true); + table.appendRow(TableRow{TableCell(u"Constant"_s), + TableCell(u"Description"_s)}); + table.setHeaderEnabled(true); } m_output.indent(); } else if (token == QXmlStreamReader::EndElement) { m_output.outdent(); - if (!m_currentTable.isEmpty()) { + const auto &table = m_tables.back(); + if (!table.isEmpty()) { switch (listType) { case BulletList: case OrderedList: { m_output << '\n'; const char *separator = listType == BulletList ? "* " : "#. "; const char *indentLine = listType == BulletList ? " " : " "; - for (const TableCell &cell : m_currentTable.constFirst()) { + 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) @@ -1023,7 +1119,7 @@ void QtXmlToSphinx::handleListTag(QXmlStreamReader& reader) break; } } - m_currentTable.clear(); + m_tables.pop(); } } @@ -1065,7 +1161,7 @@ QtXmlToSphinxLink *QtXmlToSphinx::handleLinkStart(const QString &type, QString r if (type == u"external" || isHttpLink(ref)) { result->type = QtXmlToSphinxLink::External; - } else if (type == functionLinkType() && !m_context.isEmpty()) { + } else if (type == functionLinkType && !m_context.isEmpty()) { result->type = QtXmlToSphinxLink::Method; const auto rawlinklist = QStringView{result->linkRef}.split(u'.'); if (rawlinklist.size() == 1 || rawlinklist.constFirst() == m_context) { @@ -1076,9 +1172,9 @@ QtXmlToSphinxLink *QtXmlToSphinx::handleLinkStart(const QString &type, QString r } else { result->linkRef = m_generator->expandFunction(result->linkRef); } - } else if (type == functionLinkType() && m_context.isEmpty()) { + } else if (type == functionLinkType && m_context.isEmpty()) { result->type = QtXmlToSphinxLink::Function; - } else if (type == classLinkType()) { + } else if (type == classLinkType) { result->type = QtXmlToSphinxLink::Class; result->linkRef = m_generator->expandClass(m_context, result->linkRef); } else if (type == u"enum") { @@ -1118,10 +1214,10 @@ static QString fixLinkText(const QtXmlToSphinxLink *linkContext, else QtXmlToSphinx::stripPythonQualifiers(&linktext); if (linkContext->linkRef == linktext) - return QString(); + return {}; if ((linkContext->type & QtXmlToSphinxLink::FunctionMask) != 0 && (linkContext->linkRef + u"()"_s) == linktext) { - return QString(); + return {}; } return linktext; } @@ -1149,7 +1245,7 @@ static bool copyImage(const QString &href, const QString &docDataDir, const QLoggingCategory &lc, QString *errorMessage) { const QChar slash = u'/'; - const int lastSlash = href.lastIndexOf(slash); + const auto lastSlash = href.lastIndexOf(slash); const QString imagePath = lastSlash != -1 ? href.left(lastSlash) : QString(); const QString imageFileName = lastSlash != -1 ? href.right(href.size() - lastSlash - 1) : href; QFileInfo imageSource(docDataDir + slash + href); @@ -1162,7 +1258,7 @@ static bool copyImage(const QString &href, const QString &docDataDir, // FIXME: Not perfect yet, should have knowledge about namespaces (DataVis3D) or // nested classes "Pyside2.QtGui.QTouchEvent.QTouchPoint". QString relativeTargetDir = context; - const int lastDot = relativeTargetDir.lastIndexOf(u'.'); + const auto lastDot = relativeTargetDir.lastIndexOf(u'.'); if (lastDot != -1) relativeTargetDir.truncate(lastDot); relativeTargetDir.replace(u'.', slash); @@ -1229,7 +1325,7 @@ void QtXmlToSphinx::handleInlineImageTag(QXmlStreamReader& reader) // enclosed by '|' and define it further down. Determine tag from the base //file name with number. QString tag = href; - int pos = tag.lastIndexOf(u'/'); + auto pos = tag.lastIndexOf(u'/'); if (pos != -1) tag.remove(0, pos + 1); pos = tag.indexOf(u'.'); @@ -1297,11 +1393,11 @@ void QtXmlToSphinx::handlePageTag(QXmlStreamReader &reader) m_output << disableIndent; - const auto title = reader.attributes().value(titleAttribute()); + const auto title = reader.attributes().value("title"); if (!title.isEmpty()) m_output << rstLabel(title.toString()); - const auto fullTitle = reader.attributes().value(fullTitleAttribute()); + const auto fullTitle = reader.attributes().value("fulltitle"); const int size = fullTitle.isEmpty() ? writeEscapedRstText(m_output, title) : writeEscapedRstText(m_output, fullTitle); @@ -1314,7 +1410,7 @@ void QtXmlToSphinx::handleTargetTag(QXmlStreamReader &reader) { if (reader.tokenType() != QXmlStreamReader::StartElement) return; - const auto name = reader.attributes().value(nameAttribute()); + const auto name = reader.attributes().value("name"); if (!name.isEmpty()) m_output << rstLabel(name.toString()); } @@ -1405,7 +1501,7 @@ void QtXmlToSphinx::Table::normalize() //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 : qAsConst(m_rows)) { + for (const auto &row : std::as_const(m_rows)) { if (row.size() > maxCols) maxCols = row.size(); } @@ -1489,13 +1585,11 @@ void QtXmlToSphinx::Table::format(TextStream& s) const // print line s << '+'; for (qsizetype col = 0; col < headerColumnCount; ++col) { - char c; + char c = '-'; if (col >= row.size() || row[col].rowSpan == -1) c = ' '; else if (i == 1 && hasHeader()) c = '='; - else - c = '-'; s << Pad(c, colWidths.at(col)) << '+'; } s << '\n'; |