diff options
Diffstat (limited to 'sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp')
-rw-r--r-- | sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp | 296 |
1 files changed, 173 insertions, 123 deletions
diff --git a/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp b/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp index 97e9efeef..b8fec836c 100644 --- a/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp +++ b/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp @@ -19,10 +19,6 @@ 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) { @@ -63,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 = { @@ -407,11 +417,13 @@ 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() @@ -423,19 +435,19 @@ void QtXmlToSphinx::pushOutputBuffer() QString QtXmlToSphinx::popOutputBuffer() { Q_ASSERT(!m_buffers.isEmpty()); - QString result(*m_buffers.top().get()); + QString result(*m_buffers.top()); m_buffers.pop(); m_output.setString(m_buffers.isEmpty() ? nullptr : m_buffers.top().get()); return result; } -static const QString autoTranslatedPlaceholder = u"AUTO_GENERATED\n"_s; -static const QString autoTranslatedNote = -uR"(.. warning:: +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. -)"_s; +)"_L1; void QtXmlToSphinx::setAutoTranslatedNote(QString *str) const { @@ -597,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) { @@ -606,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 @@ -616,37 +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 cppCodeSnippetCode(u"//!\\s*\\[[\\w\\d\\s]+\\]"_s); - Q_ASSERT(cppCodeSnippetCode.isValid()); - static const QRegularExpression pythonCodeSnippetCode(u"#!\\s*\\[[\\w\\d\\s]+\\]"_s); - Q_ASSERT(pythonCodeSnippetCode.isValid()); - - bool getCode = false; - - while (!inputFile.atEnd()) { - QString line = QString::fromUtf8(inputFile.readLine()); - if (getCode && !line.contains(searchString)) { - line.remove(cppCodeSnippetCode); - line.remove(pythonCodeSnippetCode); - 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) @@ -709,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"; } @@ -780,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; @@ -822,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); @@ -841,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) @@ -876,13 +941,13 @@ 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, @@ -938,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(); } @@ -958,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)); } } @@ -967,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; } @@ -991,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 }; @@ -1015,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) @@ -1049,7 +1119,7 @@ void QtXmlToSphinx::handleListTag(QXmlStreamReader& reader) break; } } - m_currentTable.clear(); + m_tables.pop(); } } @@ -1091,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) { @@ -1102,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") { @@ -1144,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; } @@ -1170,36 +1240,17 @@ WebXmlTag QtXmlToSphinx::parentTag() const // 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 &href, const QString &docDataDir, - const QString &context, const QString &outputDir, +static bool copyImage(const QString &docDataDir, const QString &relativeSourceFile, + const QString &outputDir, const QString &relativeTargetFile, const QLoggingCategory &lc, QString *errorMessage) { - const QChar slash = u'/'; - const int 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); - if (!imageSource.exists()) { - QTextStream(errorMessage) << "Image " << href << " does not exist in " - << QDir::toNativeSeparators(docDataDir); - return false; - } - // Determine 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 int lastDot = relativeTargetDir.lastIndexOf(u'.'); - if (lastDot != -1) - relativeTargetDir.truncate(lastDot); - relativeTargetDir.replace(u'.', slash); - if (!imagePath.isEmpty()) - relativeTargetDir += slash + imagePath; - - const QString targetDir = outputDir + slash + relativeTargetDir; - const QString targetFileName = targetDir + slash + imageFileName; + QString targetFileName = outputDir + u'/' + relativeTargetFile; if (QFileInfo::exists(targetFileName)) return true; - if (!QFileInfo::exists(targetDir)) { + + 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) @@ -1208,28 +1259,29 @@ static bool copyImage(const QString &href, const QString &docDataDir, } } - QFile source(imageSource.absoluteFilePath()); + 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__ << " href=\"" - << href << "\", context=\"" << context << "\", docDataDir=\"" - << docDataDir << "\", outputDir=\"" << outputDir << "\", copied \"" - << source.fileName() << "\"->\"" << targetFileName << '"'; + + qCDebug(lc).noquote().nospace() << __FUNCTION__ << " \"" << relativeSourceFile + << "\"->\"" << relativeTargetFile << '"'; return true; } bool QtXmlToSphinx::copyImage(const QString &href) const { QString errorMessage; - const bool result = - ::copyImage(href, m_parameters.docDataDir, m_context, - m_parameters.outputDirectory, - m_generator->loggingCategory(), - &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; @@ -1255,7 +1307,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'.'); @@ -1323,11 +1375,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); @@ -1340,7 +1392,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()); } @@ -1515,13 +1567,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'; |