diff options
Diffstat (limited to 'sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp')
-rw-r--r-- | sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp | 959 |
1 files changed, 616 insertions, 343 deletions
diff --git a/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp b/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp index 20681f426..55c1d2090 100644 --- a/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp +++ b/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp @@ -1,45 +1,23 @@ -/**************************************************************************** -** -** Copyright (C) 2020 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of Qt for Python. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "qtxmltosphinx.h" +#include "exception.h" #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> -static inline QString nameAttribute() { return QStringLiteral("name"); } -static inline QString titleAttribute() { return QStringLiteral("title"); } -static inline QString fullTitleAttribute() { return QStringLiteral("fulltitle"); } +using namespace Qt::StringLiterals; QString msgTagWarning(const QXmlStreamReader &reader, const QString &context, const QString &tag, const QString &message) @@ -57,78 +35,130 @@ QString msgTagWarning(const QXmlStreamReader &reader, const QString &context, return result; } -QString msgFallbackWarning(const QXmlStreamReader &reader, const QString &context, - const QString &tag, const QString &location, - const QString &identifier, const QString &fallback) +QString msgFallbackWarning(const QString &location, const QString &identifier, + const QString &fallback) { - QString message = QLatin1String("Falling back to \"") - + QDir::toNativeSeparators(fallback) + QLatin1String("\" for \"") - + location + QLatin1Char('"'); + QString message = u"Falling back to \""_s + + QDir::toNativeSeparators(fallback) + u"\" for \""_s + + location + u'"'; if (!identifier.isEmpty()) - message += QLatin1String(" [") + identifier + QLatin1Char(']'); - return msgTagWarning(reader, context, tag, message); + message += u" ["_s + identifier + u']'; + return message; } -struct QtXmlToSphinx::LinkContext +QString msgSnippetsResolveError(const QString &path, const QStringList &locations) { - enum Type - { - Method = 0x1, Function = 0x2, - FunctionMask = Method | Function, - Class = 0x4, Attribute = 0x8, Module = 0x10, - Reference = 0x20, External= 0x40 + 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"}, }; - enum Flags { InsideBold = 0x1, InsideItalic = 0x2 }; + 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; +} - explicit LinkContext(const QString &ref) : linkRef(ref) {} +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; +} - QString linkRef; - QString linkText; - Type type = Reference; - int flags = 0; -}; +QDebug operator<<(QDebug debug, const QtXmlToSphinx::Table &t) +{ + QDebugStateSaver saver(debug); + debug.noquote(); + debug.nospace(); + t.formatDebug(debug); + return debug; +} -static const char *linkKeyWord(QtXmlToSphinx::LinkContext::Type type) +static const char *linkKeyWord(QtXmlToSphinxLink::Type type) { switch (type) { - case QtXmlToSphinx::LinkContext::Method: + case QtXmlToSphinxLink::Method: return ":meth:"; - case QtXmlToSphinx::LinkContext::Function: + case QtXmlToSphinxLink::Function: return ":func:"; - case QtXmlToSphinx::LinkContext::Class: + case QtXmlToSphinxLink::Class: return ":class:"; - case QtXmlToSphinx::LinkContext::Attribute: + case QtXmlToSphinxLink::Attribute: return ":attr:"; - case QtXmlToSphinx::LinkContext::Module: + case QtXmlToSphinxLink::Module: return ":mod:"; - case QtXmlToSphinx::LinkContext::Reference: + case QtXmlToSphinxLink::Reference: return ":ref:"; - case QtXmlToSphinx::LinkContext::External: + case QtXmlToSphinxLink::External: break; - case QtXmlToSphinx::LinkContext::FunctionMask: + case QtXmlToSphinxLink::FunctionMask: break; } return ""; } -TextStream &operator<<(TextStream &str, const QtXmlToSphinx::LinkContext &linkContext) +TextStream &operator<<(TextStream &str, const QtXmlToSphinxLink &linkContext) { // Temporarily turn off bold/italic since links do not work within - if (linkContext.flags & QtXmlToSphinx::LinkContext::InsideBold) + if (linkContext.flags & QtXmlToSphinxLink::InsideBold) str << "**"; - else if (linkContext.flags & QtXmlToSphinx::LinkContext::InsideItalic) + else if (linkContext.flags & QtXmlToSphinxLink::InsideItalic) str << '*'; str << ' ' << linkKeyWord(linkContext.type) << '`'; - const bool isExternal = linkContext.type == QtXmlToSphinx::LinkContext::External; + const bool isExternal = linkContext.type == QtXmlToSphinxLink::External; if (!linkContext.linkText.isEmpty()) { writeEscapedRstText(str, linkContext.linkText); - if (isExternal && !linkContext.linkText.endsWith(QLatin1Char(' '))) + if (isExternal && !linkContext.linkText.endsWith(u' ')) str << ' '; str << '<'; } // Convert page titles to RST labels - str << (linkContext.type == QtXmlToSphinx::LinkContext::Reference + str << (linkContext.type == QtXmlToSphinxLink::Reference ? toRstLabel(linkContext.linkRef) : linkContext.linkRef); if (!linkContext.linkText.isEmpty()) str << '>'; @@ -136,9 +166,9 @@ TextStream &operator<<(TextStream &str, const QtXmlToSphinx::LinkContext &linkCo if (isExternal) str << '_'; str << ' '; - if (linkContext.flags & QtXmlToSphinx::LinkContext::InsideBold) + if (linkContext.flags & QtXmlToSphinxLink::InsideBold) str << "**"; - else if (linkContext.flags & QtXmlToSphinx::LinkContext::InsideItalic) + else if (linkContext.flags & QtXmlToSphinxLink::InsideItalic) str << '*'; return str; } @@ -237,9 +267,8 @@ QtXmlToSphinx::QtXmlToSphinx(const QtXmlToSphinxDocGeneratorInterface *docGenera const QtXmlToSphinxParameters ¶meters, const QString& doc, const QString& context) : m_output(static_cast<QString *>(nullptr)), - m_tableHasHeader(false), m_context(context), - m_generator(docGenerator), m_parameters(parameters), - m_insideBold(false), m_insideItalic(false) + m_context(context), + m_generator(docGenerator), m_parameters(parameters) { m_result = transform(doc); } @@ -276,7 +305,7 @@ void QtXmlToSphinx::callHandler(WebXmlTag t, QXmlStreamReader &r) handleTableTag(r); break; case WebXmlTag::header: - handleRowTag(r); + handleHeaderTag(r); break; case WebXmlTag::row: handleRowTag(r); @@ -388,35 +417,49 @@ void QtXmlToSphinx::callHandler(WebXmlTag t, QXmlStreamReader &r) void QtXmlToSphinx::formatCurrentTable() { - if (m_currentTable.isEmpty()) + Q_ASSERT(!m_tables.isEmpty()); + auto &table = m_tables.back(); + if (table.isEmpty()) return; - m_currentTable.setHeaderEnabled(m_tableHasHeader); - m_currentTable.normalize(); + table.normalize(); m_output << '\n'; - m_currentTable.format(m_output); + table.format(m_output); } void QtXmlToSphinx::pushOutputBuffer() { - auto *buffer = new QString(); - m_buffers << buffer; - m_output.setString(buffer); + m_buffers.append(std::make_shared<QString>()); + m_output.setString(m_buffers.top().get()); } QString QtXmlToSphinx::popOutputBuffer() { Q_ASSERT(!m_buffers.isEmpty()); - QString* str = m_buffers.pop(); - QString strcpy(*str); - delete str; - m_output.setString(m_buffers.isEmpty() ? 0 : m_buffers.top()); - return strcpy; + 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()); - Indentation indentation(m_output); if (doc.trimmed().isEmpty()) return doc; @@ -424,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()) { @@ -432,7 +478,7 @@ QString QtXmlToSphinx::transform(const QString& doc) << reader.errorString() << " at " << reader.lineNumber() << ':' << reader.columnNumber() << '\n' << doc; m_output << message; - warn(message); + throw Exception(message); break; } @@ -455,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(); @@ -464,13 +510,14 @@ QString QtXmlToSphinx::transform(const QString& doc) m_output.flush(); QString retval = popOutputBuffer(); Q_ASSERT(m_buffers.isEmpty()); + setAutoTranslatedNote(&retval); return retval; } static QString resolveFile(const QStringList &locations, const QString &path) { for (QString location : locations) { - location.append(QLatin1Char('/')); + location.append(u'/'); location.append(path); if (QFileInfo::exists(location)) return location; @@ -478,25 +525,170 @@ static QString resolveFile(const QStringList &locations, const QString &path) return QString(); } -QString QtXmlToSphinx::readFromLocations(const QStringList &locations, const QString &path, - const QString &identifier, QString *errorMessage) +enum class SnippetType +{ + Other, // .qdoc, .qml,... + CppSource, CppHeader // Potentially converted to Python +}; + +SnippetType snippetType(const QString &path) +{ + if (path.endsWith(u".cpp")) + return SnippetType::CppSource; + if (path.endsWith(u".h")) + return SnippetType::CppHeader; + return SnippetType::Other; +} + +// Return the name of a .cpp/.h snippet converted to Python by snippets-translate +static QString pySnippetName(const QString &path, SnippetType type) { + switch (type) { + case SnippetType::CppSource: + return path.left(path.size() - 3) + u"py"_s; + break; + case SnippetType::CppHeader: + return path + u".py"_s; + break; + default: + break; + } + return {}; +} + +QtXmlToSphinx::Snippet QtXmlToSphinx::readSnippetFromLocations(const QString &path, + const QString &identifier, + const QString &fallbackPath, + QString *errorMessage) +{ + // For anything else but C++ header/sources (no conversion to Python), + // use existing fallback paths first. + const auto type = snippetType(path); + if (type == SnippetType::Other && !fallbackPath.isEmpty()) { + const QString code = readFromLocation(fallbackPath, identifier, errorMessage); + return {code, code.isNull() ? Snippet::Error : Snippet::Fallback}; + } + + // For C++ header/sources, try snippets converted to Python first. QString resolvedPath; - if (path.endsWith(QLatin1String(".cpp"))) { - const QString pySnippet = path.left(path.size() - 3) + QLatin1String("py"); - resolvedPath = resolveFile(locations, pySnippet); + const auto &locations = m_parameters.codeSnippetDirs; + + if (type != SnippetType::Other) { + if (!fallbackPath.isEmpty() && !m_parameters.codeSnippetRewriteOld.isEmpty()) { + // Try looking up Python converted snippets by rewriting snippets paths + QString rewrittenPath = pySnippetName(fallbackPath, type); + if (!rewrittenPath.isEmpty()) { + rewrittenPath.replace(m_parameters.codeSnippetRewriteOld, + m_parameters.codeSnippetRewriteNew); + const QString code = readFromLocation(rewrittenPath, identifier, errorMessage); + m_containsAutoTranslations = true; + return {code, code.isNull() ? Snippet::Error : Snippet::Converted}; + } + } + + resolvedPath = resolveFile(locations, pySnippetName(path, type)); + if (!resolvedPath.isEmpty()) { + const QString code = readFromLocation(resolvedPath, identifier, errorMessage); + return {code, code.isNull() ? Snippet::Error : Snippet::Converted}; + } + } + + resolvedPath = resolveFile(locations, path); + if (!resolvedPath.isEmpty()) { + const QString code = readFromLocation(resolvedPath, identifier, errorMessage); + return {code, code.isNull() ? Snippet::Error : Snippet::Resolved}; + } + + if (!fallbackPath.isEmpty()) { + *errorMessage = msgFallbackWarning(path, identifier, fallbackPath); + const QString code = readFromLocation(fallbackPath, identifier, errorMessage); + return {code, code.isNull() ? Snippet::Error : Snippet::Fallback}; } - if (resolvedPath.isEmpty()) - resolvedPath = resolveFile(locations, path); - if (resolvedPath.isEmpty()) { - QTextStream(errorMessage) << "Could not resolve \"" << path << "\" in \"" - << locations.join(QLatin1String("\", \"")); - return QString(); // null + + *errorMessage = msgSnippetsResolveError(path, locations); + return {{}, Snippet::Error}; +} + +// Helpers for extracting qdoc snippets "#/// [id]" +static QString fileNameOfDevice(const QIODevice *inputFile) +{ + const auto *file = qobject_cast<const QFile *>(inputFile); + return file ? QDir::toNativeSeparators(file->fileName()) : u"<stdin>"_s; +} + +static QString msgSnippetNotFound(const QIODevice &inputFile, + const QString &identifier) +{ + return u"Code snippet file found ("_s + fileNameOfDevice(&inputFile) + + u"), but snippet ["_s + identifier + u"] not found."_s; +} + +static QString msgEmptySnippet(const QIODevice &inputFile, int lineNo, + const QString &identifier) +{ + return u"Empty code snippet ["_s + identifier + u"] at "_s + + fileNameOfDevice(&inputFile) + u':' + QString::number(lineNo); +} + +// Pattern to match qdoc snippet IDs with "#/// [id]" comments and helper to find ID +static const QRegularExpression &snippetIdPattern() +{ + static const QRegularExpression result(uR"RX((//|#) *! *\[([^]]+)\])RX"_s); + Q_ASSERT(result.isValid()); + return result; +} + +static bool matchesSnippetId(QRegularExpressionMatchIterator it, + const QString &identifier) +{ + while (it.hasNext()) { + if (it.next().captured(2) == identifier) + return true; } - qCDebug(m_generator->loggingCategory()).noquote().nospace() - << "snippet file " << path - << " [" << identifier << ']' << " resolved to " << resolvedPath; - return readFromLocation(resolvedPath, identifier, errorMessage); + 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, @@ -508,44 +700,18 @@ QString QtXmlToSphinx::readFromLocation(const QString &location, const QString & QTextStream(errorMessage) << "Could not read code snippet file: " << QDir::toNativeSeparators(inputFile.fileName()) << ": " << inputFile.errorString(); - return QString(); // null + return {}; // null } - QString code = QLatin1String(""); // non-null + QString code = u""_s; // non-null if (identifier.isEmpty()) { while (!inputFile.atEnd()) code += QString::fromUtf8(inputFile.readLine()); - return code; - } - - const QRegularExpression searchString(QLatin1String("//!\\s*\\[") - + identifier + QLatin1String("\\]")); - Q_ASSERT(searchString.isValid()); - static const QRegularExpression codeSnippetCode(QLatin1String("//!\\s*\\[[\\w\\d\\s]+\\]")); - Q_ASSERT(codeSnippetCode.isValid()); - - bool getCode = false; - - while (!inputFile.atEnd()) { - QString line = QString::fromUtf8(inputFile.readLine()); - if (getCode && !line.contains(searchString)) { - line.remove(codeSnippetCode); - code += line; - } else if (line.contains(searchString)) { - if (getCode) - break; - getCode = true; - } + return CodeSnipHelpers::fixSpaces(code); } - if (!getCode) { - QTextStream(errorMessage) << "Code snippet file found (" - << QDir::toNativeSeparators(location) << "), but snippet [" - << identifier << "] not found."; - return QString(); // null - } - - return code; + code = readSnippet(inputFile, identifier, errorMessage); + return code.isEmpty() ? QString{} : CodeSnipHelpers::fixSpaces(code); // maintain isNull() } void QtXmlToSphinx::handleHeadingTag(QXmlStreamReader& reader) @@ -555,7 +721,7 @@ void QtXmlToSphinx::handleHeadingTag(QXmlStreamReader& reader) static char types[] = { '-', '^' }; QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { - uint typeIdx = reader.attributes().value(QLatin1String("level")).toUInt(); + uint typeIdx = reader.attributes().value(u"level"_s).toUInt(); if (typeIdx >= sizeof(types)) type = types[sizeof(types)-1]; else @@ -572,80 +738,133 @@ void QtXmlToSphinx::handleHeadingTag(QXmlStreamReader& reader) void QtXmlToSphinx::handleParaTag(QXmlStreamReader& reader) { - QXmlStreamReader::TokenType token = reader.tokenType(); - if (token == QXmlStreamReader::StartElement) { - pushOutputBuffer(); - } else if (token == QXmlStreamReader::EndElement) { - QString result = popOutputBuffer().simplified(); - if (result.startsWith(QLatin1String("**Warning:**"))) - result.replace(0, 12, QLatin1String(".. warning:: ")); - else if (result.startsWith(QLatin1String("**Note:**"))) - result.replace(0, 9, QLatin1String(".. note:: ")); + switch (reader.tokenType()) { + case QXmlStreamReader::StartElement: + handleParaTagStart(); + break; + case QXmlStreamReader::EndElement: + handleParaTagEnd(); + break; + case QXmlStreamReader::Characters: + handleParaTagText(reader); + break; + default: + break; + } +} - m_output << result << "\n\n"; - } else if (token == QXmlStreamReader::Characters) { - 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 == QLatin1Char('*') || end == QLatin1Char('`')) && start != QLatin1Char(' ') && !start.isPunct()) - m_output << '\\'; - } - m_output << escape(text); +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) { - QXmlStreamReader::TokenType token = reader.tokenType(); - if (token == QXmlStreamReader::StartElement || token == QXmlStreamReader::EndElement) { - m_insideItalic = !m_insideItalic; - m_output << '*'; - } else if (token == QXmlStreamReader::Characters) { + switch (reader.tokenType()) { + case QXmlStreamReader::StartElement: + if (m_formattingDepth++ == 0) { + m_insideItalic = true; + m_output << rstItalic; + } + break; + case QXmlStreamReader::EndElement: + if (--m_formattingDepth == 0) { + m_insideItalic = false; + m_output << rstItalicOff; + } + break; + case QXmlStreamReader::Characters: m_output << escape(reader.text().trimmed()); + break; + default: + break; } } void QtXmlToSphinx::handleBoldTag(QXmlStreamReader& reader) { - QXmlStreamReader::TokenType token = reader.tokenType(); - if (token == QXmlStreamReader::StartElement || token == QXmlStreamReader::EndElement) { - m_insideBold = !m_insideBold; - m_output << "**"; - } else if (token == QXmlStreamReader::Characters) { + switch (reader.tokenType()) { + case QXmlStreamReader::StartElement: + if (m_formattingDepth++ == 0) { + m_insideBold = true; + m_output << rstBold; + } + break; + case QXmlStreamReader::EndElement: + if (--m_formattingDepth == 0) { + m_insideBold = false; + m_output << rstBoldOff; + } + break; + case QXmlStreamReader::Characters: m_output << escape(reader.text().trimmed()); + break; + default: + break; } } void QtXmlToSphinx::handleArgumentTag(QXmlStreamReader& reader) { - QXmlStreamReader::TokenType token = reader.tokenType(); - if (token == QXmlStreamReader::StartElement || token == QXmlStreamReader::EndElement) - m_output << "``"; - else if (token == QXmlStreamReader::Characters) + switch (reader.tokenType()) { + case QXmlStreamReader::StartElement: + if (m_formattingDepth++ == 0) + m_output << rstCode; + break; + case QXmlStreamReader::EndElement: + if (--m_formattingDepth == 0) + m_output << rstCodeOff; + break; + case QXmlStreamReader::Characters: m_output << reader.text().trimmed(); + break; + default: + break; + } } -static inline QString functionLinkType() { return QStringLiteral("function"); } -static inline QString classLinkType() { return QStringLiteral("class"); } +constexpr auto functionLinkType = "function"_L1; +constexpr auto classLinkType = "class"_L1; static inline QString fixLinkType(QStringView type) { // TODO: create a flag PROPERTY-AS-FUNCTION to ask if the properties // are recognized as such or not in the binding - if (type == QLatin1String("property")) - return functionLinkType(); - if (type == QLatin1String("typedef")) - return classLinkType(); + if (type == u"property") + return functionLinkType; + if (type == u"typedef") + return classLinkType; return type.toString(); } static inline QString linkSourceAttribute(const QString &type) { - if (type == functionLinkType() || type == classLinkType()) - return QLatin1String("raw"); - return type == QLatin1String("enum") || type == QLatin1String("page") - ? type : QLatin1String("href"); + if (type == functionLinkType || type == classLinkType) + return u"raw"_s; + return type == u"enum" || type == u"page" + ? type : u"href"_s; } // "See also" links may appear as nested links: @@ -667,8 +886,8 @@ void QtXmlToSphinx::handleSeeAlsoTag(QXmlStreamReader& reader) if (!textR.isEmpty()) { const QString text = textR.toString(); if (m_seeAlsoContext.isNull()) { - const QString type = text.endsWith(QLatin1String("()")) - ? functionLinkType() : classLinkType(); + const QString type = text.endsWith(u"()") + ? functionLinkType : classLinkType; m_seeAlsoContext.reset(handleLinkStart(type, text)); } handleLinkText(m_seeAlsoContext.data(), text); @@ -687,12 +906,12 @@ void QtXmlToSphinx::handleSeeAlsoTag(QXmlStreamReader& reader) } } -static inline QString fallbackPathAttribute() { return QStringLiteral("path"); } +constexpr auto fallbackPathAttribute = "path"_L1; template <class Indent> // const char*/class Indentor void formatSnippet(TextStream &str, Indent indent, const QString &snippet) { - const auto lines = QStringView{snippet}.split(QLatin1Char('\n')); + const auto lines = QStringView{snippet}.split(u'\n'); for (const auto &line : lines) { if (!line.trimmed().isEmpty()) str << indent << line; @@ -718,68 +937,61 @@ void QtXmlToSphinx::handleSnippetTag(QXmlStreamReader& reader) { QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { - const bool consecutiveSnippet = m_lastTagName == QLatin1String("snippet") - || m_lastTagName == QLatin1String("dots") || m_lastTagName == QLatin1String("codeline"); + const bool consecutiveSnippet = m_lastTagName == u"snippet" + || m_lastTagName == u"dots" || m_lastTagName == u"codeline"; if (consecutiveSnippet) { m_output.flush(); - m_output.string()->chop(2); + m_output.string()->chop(1); // Strip newline from previous snippet } - QString location = reader.attributes().value(QLatin1String("location")).toString(); - QString identifier = reader.attributes().value(QLatin1String("identifier")).toString(); + QString location = reader.attributes().value(u"location"_s).toString(); + QString identifier = reader.attributes().value(u"identifier"_s).toString(); + QString fallbackPath; + if (reader.attributes().hasAttribute(fallbackPathAttribute)) + fallbackPath = reader.attributes().value(fallbackPathAttribute).toString(); QString errorMessage; - const QString pythonCode = - readFromLocations(m_parameters.codeSnippetDirs, location, identifier, &errorMessage); + + const Snippet snippet = readSnippetFromLocations(location, identifier, + fallbackPath, &errorMessage); if (!errorMessage.isEmpty()) warn(msgTagWarning(reader, m_context, m_lastTagName, errorMessage)); - // Fall back to C++ snippet when "path" attribute is present. - // Also read fallback snippet when comparison is desired. - QString fallbackCode; - if ((pythonCode.isEmpty() || m_parameters.snippetComparison) - && reader.attributes().hasAttribute(fallbackPathAttribute())) { - const QString fallback = reader.attributes().value(fallbackPathAttribute()).toString(); - if (QFileInfo::exists(fallback)) { - if (pythonCode.isEmpty()) - warn(msgFallbackWarning(reader, m_context, m_lastTagName, location, identifier, fallback)); - fallbackCode = readFromLocation(fallback, identifier, &errorMessage); - if (!errorMessage.isEmpty()) - warn(msgTagWarning(reader, m_context, m_lastTagName, errorMessage)); - } - } - if (!pythonCode.isEmpty() && !fallbackCode.isEmpty() && m_parameters.snippetComparison) - debug(msgSnippetComparison(location, identifier, pythonCode, fallbackCode)); + if (m_parameters.snippetComparison && snippet.result == Snippet::Converted + && !fallbackPath.isEmpty()) { + const QString fallbackCode = readFromLocation(fallbackPath, identifier, &errorMessage); + debug(msgSnippetComparison(location, identifier, snippet.code, fallbackCode)); + } if (!consecutiveSnippet) m_output << "::\n\n"; Indentation indentation(m_output); - const QString code = pythonCode.isEmpty() ? fallbackCode : pythonCode; - if (code.isEmpty()) + if (snippet.result == Snippet::Error) m_output << "<Code snippet \"" << location << ':' << identifier << "\" not found>\n"; else - m_output << code << ensureEndl; + m_output << snippet.code << ensureEndl; m_output << '\n'; } } + void QtXmlToSphinx::handleDotsTag(QXmlStreamReader& reader) { QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { - const bool consecutiveSnippet = m_lastTagName == QLatin1String("snippet") - || m_lastTagName == QLatin1String("dots") || m_lastTagName == QLatin1String("codeline"); + const bool consecutiveSnippet = m_lastTagName == u"snippet" + || m_lastTagName == u"dots" || m_lastTagName == u"codeline"; if (consecutiveSnippet) { m_output.flush(); m_output.string()->chop(2); } else { m_output << "::\n\n"; } - Indentation indentation(m_output); pushOutputBuffer(); - int indent = reader.attributes().value(QLatin1String("indent")).toInt(); + int indent = reader.attributes().value(u"indent"_s).toInt() + + m_output.indentation() * m_output.tabWidth(); for (int i = 0; i < indent; ++i) m_output << ' '; } else if (token == QXmlStreamReader::Characters) { - m_output << reader.text().toString(); + m_output << reader.text().toString().trimmed(); } else if (token == QXmlStreamReader::EndElement) { m_output << disableIndent << popOutputBuffer() << "\n\n\n" << enableIndent; } @@ -789,12 +1001,15 @@ void QtXmlToSphinx::handleTableTag(QXmlStreamReader& reader) { QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { - m_currentTable.clear(); - m_tableHasHeader = false; + 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_currentTable.clear(); + m_tables.pop(); + if (parentTag() == WebXmlTag::para) + handleParaTagStart(); } } @@ -804,11 +1019,11 @@ void QtXmlToSphinx::handleTermTag(QXmlStreamReader& reader) if (token == QXmlStreamReader::StartElement) { pushOutputBuffer(); } else if (token == QXmlStreamReader::Characters) { - m_output << reader.text().toString().replace(QLatin1String("::"), QLatin1String(".")); + m_output << reader.text().toString().replace(u"::"_s, u"."_s); } else if (token == QXmlStreamReader::EndElement) { TableCell cell; cell.data = popOutputBuffer().trimmed(); - m_currentTable.appendRow(TableRow(1, cell)); + m_tables.back().appendRow(TableRow(1, cell)); } } @@ -817,70 +1032,83 @@ void QtXmlToSphinx::handleItemTag(QXmlStreamReader& reader) { QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { - if (m_currentTable.isEmpty()) - m_currentTable.appendRow({}); - TableRow& row = m_currentTable.last(); + auto &table = m_tables.back(); + if (table.isEmpty()) + table.appendRow({}); + TableRow& row = table.last(); TableCell cell; - cell.colSpan = reader.attributes().value(QLatin1String("colspan")).toShort(); - cell.rowSpan = reader.attributes().value(QLatin1String("rowspan")).toShort(); + cell.colSpan = reader.attributes().value(u"colspan"_s).toShort(); + cell.rowSpan = reader.attributes().value(u"rowspan"_s).toShort(); row << cell; pushOutputBuffer(); } else if (token == QXmlStreamReader::EndElement) { - QString data = popOutputBuffer().trimmed(); - if (!m_currentTable.isEmpty()) { - TableRow& row = m_currentTable.last(); + QString data = trimLeadingNewlines(trimRight(popOutputBuffer())); + auto &table = m_tables.back(); + if (!table.isEmpty()) { + TableRow& row = table.last(); if (!row.isEmpty()) row.last().data = data; } } } -void QtXmlToSphinx::handleRowTag(QXmlStreamReader& reader) +void QtXmlToSphinx::handleHeaderTag(QXmlStreamReader &reader) { - QXmlStreamReader::TokenType token = reader.tokenType(); - if (token == QXmlStreamReader::StartElement) { - m_tableHasHeader = reader.name() == QLatin1String("header"); - m_currentTable.appendRow({}); + // <header> in WebXML is either a table header or a description of a + // C++ header with "name"/"href" attributes. + if (reader.tokenType() == QXmlStreamReader::StartElement + && !reader.attributes().hasAttribute(u"name"_s)) { + auto &table = m_tables.back(); + table.setHeaderEnabled(true); + table.appendRow({}); } } +void QtXmlToSphinx::handleRowTag(QXmlStreamReader& reader) +{ + if (reader.tokenType() == QXmlStreamReader::StartElement) + m_tables.back().appendRow({}); +} + enum ListType { BulletList, OrderedList, EnumeratedList }; static inline ListType webXmlListType(QStringView t) { - if (t == QLatin1String("enum")) + if (t == u"enum") return EnumeratedList; - if (t == QLatin1String("ordered")) + if (t == u"ordered") return OrderedList; return BulletList; } void QtXmlToSphinx::handleListTag(QXmlStreamReader& reader) { - // BUG We do not support a list inside a table cell static ListType listType = BulletList; QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { - listType = webXmlListType(reader.attributes().value(QLatin1String("type"))); + m_tables.push({}); + auto &table = m_tables.back(); + listType = webXmlListType(reader.attributes().value(u"type"_s)); if (listType == EnumeratedList) { - m_currentTable.appendRow(TableRow{TableCell(QLatin1String("Constant")), - TableCell(QLatin1String("Description"))}); - m_tableHasHeader = true; + table.appendRow(TableRow{TableCell(u"Constant"_s), + TableCell(u"Description"_s)}); + table.setHeaderEnabled(true); } m_output.indent(); } else if (token == QXmlStreamReader::EndElement) { m_output.outdent(); - if (!m_currentTable.isEmpty()) { + const auto &table = m_tables.back(); + if (!table.isEmpty()) { switch (listType) { case BulletList: case OrderedList: { m_output << '\n'; const char *separator = listType == BulletList ? "* " : "#. "; const char *indentLine = listType == BulletList ? " " : " "; - for (const TableCell &cell : m_currentTable.constFirst()) { - const auto itemLines = QStringView{cell.data}.split(QLatin1Char('\n')); + for (const TableCell &cell : table.constFirst()) { + const auto itemLines = QStringView{cell.data}.split(u'\n'); m_output << separator << itemLines.constFirst() << '\n'; - for (int i = 1, max = itemLines.count(); i < max; ++i) + for (qsizetype i = 1, max = itemLines.size(); i < max; ++i) m_output << indentLine << itemLines[i] << '\n'; } m_output << '\n'; @@ -891,7 +1119,7 @@ void QtXmlToSphinx::handleListTag(QXmlStreamReader& reader) break; } } - m_currentTable.clear(); + m_tables.pop(); } } @@ -901,7 +1129,7 @@ void QtXmlToSphinx::handleLinkTag(QXmlStreamReader& reader) case QXmlStreamReader::StartElement: { // <link> embedded in <see-also> means the characters of <see-also> are no link. m_seeAlsoContext.reset(); - const QString type = fixLinkType(reader.attributes().value(QLatin1String("type"))); + const QString type = fixLinkType(reader.attributes().value(u"type"_s)); const QString ref = reader.attributes().value(linkSourceAttribute(type)).toString(); m_linkContext.reset(handleLinkStart(type, ref)); } @@ -920,90 +1148,94 @@ void QtXmlToSphinx::handleLinkTag(QXmlStreamReader& reader) } } -QtXmlToSphinx::LinkContext *QtXmlToSphinx::handleLinkStart(const QString &type, QString ref) const +QtXmlToSphinxLink *QtXmlToSphinx::handleLinkStart(const QString &type, QString ref) const { - ref.replace(QLatin1String("::"), QLatin1String(".")); - ref.remove(QLatin1String("()")); - auto *result = new LinkContext(ref); + ref.replace(u"::"_s, u"."_s); + ref.remove(u"()"_s); + auto *result = new QtXmlToSphinxLink(ref); if (m_insideBold) - result->flags |= LinkContext::InsideBold; + result->flags |= QtXmlToSphinxLink::InsideBold; else if (m_insideItalic) - result->flags |= LinkContext::InsideItalic; + result->flags |= QtXmlToSphinxLink::InsideItalic; - if (type == functionLinkType() && !m_context.isEmpty()) { - result->type = LinkContext::Method; - const auto rawlinklist = QStringView{result->linkRef}.split(QLatin1Char('.')); + 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 + QLatin1Char('.')); + result->linkRef.prepend(context + u'.'); } else { result->linkRef = m_generator->expandFunction(result->linkRef); } - } else if (type == functionLinkType() && m_context.isEmpty()) { - result->type = LinkContext::Function; - } else if (type == classLinkType()) { - result->type = LinkContext::Class; + } 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 == QLatin1String("enum")) { - result->type = LinkContext::Attribute; - } else if (type == QLatin1String("page")) { + } 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 = LinkContext::Module; - else if (result->linkRef.startsWith(QLatin1String("http"))) - result->type = LinkContext::External; + result->type = QtXmlToSphinxLink::Module; else - result->type = LinkContext::Reference; - } else if (type == QLatin1String("external")) { - result->type = LinkContext::External; + result->type = QtXmlToSphinxLink::Reference; } else { - result->type = LinkContext::Reference; + 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="http://doc.qt.io/qt-5/class.html">QML types</link> +// <link type="page" page="https://doc.qt.io/qt-5/class.html">QML types</link> // <link raw="Qt Quick" href="qtquick-index.html" type="page" page="Qt Quick">Qt Quick</link> // <link raw="QObject" href="qobject.html" type="class">QObject</link> // <link raw="Qt::Window" href="qt.html#WindowType-enum" type="enum" enum="Qt::WindowType">Qt::Window</link> // <link raw="QNetworkSession::reject()" href="qnetworksession.html#reject" type="function">QNetworkSession::reject()</link> -static QString fixLinkText(const QtXmlToSphinx::LinkContext *linkContext, +static QString fixLinkText(const QtXmlToSphinxLink *linkContext, QString linktext) { - if (linkContext->type == QtXmlToSphinx::LinkContext::External - || linkContext->type == QtXmlToSphinx::LinkContext::Reference) { + 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(QLatin1String("::")); + const int lastSep = linktext.lastIndexOf(u"::"); if (lastSep != -1) linktext.remove(0, lastSep + 2); else QtXmlToSphinx::stripPythonQualifiers(&linktext); if (linkContext->linkRef == linktext) - return QString(); - if ((linkContext->type & QtXmlToSphinx::LinkContext::FunctionMask) != 0 - && (linkContext->linkRef + QLatin1String("()")) == linktext) { - return QString(); + return {}; + if ((linkContext->type & QtXmlToSphinxLink::FunctionMask) != 0 + && (linkContext->linkRef + u"()"_s) == linktext) { + return {}; } return linktext; } -void QtXmlToSphinx::handleLinkText(LinkContext *linkContext, const QString &linktext) +void QtXmlToSphinx::handleLinkText(QtXmlToSphinxLink *linkContext, const QString &linktext) { linkContext->linkText = fixLinkText(linkContext, linktext); } -void QtXmlToSphinx::handleLinkEnd(LinkContext *linkContext) +void QtXmlToSphinx::handleLinkEnd(QtXmlToSphinxLink *linkContext) +{ + m_output << m_generator->resolveLink(*linkContext); +} + +WebXmlTag QtXmlToSphinx::parentTag() const { - m_output << *linkContext; + 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 @@ -1012,8 +1244,8 @@ static bool copyImage(const QString &href, const QString &docDataDir, const QString &context, const QString &outputDir, const QLoggingCategory &lc, QString *errorMessage) { - const QChar slash = QLatin1Char('/'); - const int lastSlash = href.lastIndexOf(slash); + const QChar slash = u'/'; + const auto lastSlash = href.lastIndexOf(slash); const QString imagePath = lastSlash != -1 ? href.left(lastSlash) : QString(); const QString imageFileName = lastSlash != -1 ? href.right(href.size() - lastSlash - 1) : href; QFileInfo imageSource(docDataDir + slash + href); @@ -1026,10 +1258,10 @@ static bool copyImage(const QString &href, const QString &docDataDir, // FIXME: Not perfect yet, should have knowledge about namespaces (DataVis3D) or // nested classes "Pyside2.QtGui.QTouchEvent.QTouchPoint". QString relativeTargetDir = context; - const int lastDot = relativeTargetDir.lastIndexOf(QLatin1Char('.')); + const auto lastDot = relativeTargetDir.lastIndexOf(u'.'); if (lastDot != -1) relativeTargetDir.truncate(lastDot); - relativeTargetDir.replace(QLatin1Char('.'), slash); + relativeTargetDir.replace(u'.', slash); if (!imagePath.isEmpty()) relativeTargetDir += slash + imagePath; @@ -1069,7 +1301,7 @@ bool QtXmlToSphinx::copyImage(const QString &href) const m_generator->loggingCategory(), &errorMessage); if (!result) - warn(errorMessage); + throw Exception(errorMessage); return result; } @@ -1077,7 +1309,7 @@ void QtXmlToSphinx::handleImageTag(QXmlStreamReader& reader) { if (reader.tokenType() != QXmlStreamReader::StartElement) return; - const QString href = reader.attributes().value(QLatin1String("href")).toString(); + const QString href = reader.attributes().value(u"href"_s).toString(); if (copyImage(href)) m_output << ".. image:: " << href << "\n\n"; } @@ -1086,17 +1318,17 @@ void QtXmlToSphinx::handleInlineImageTag(QXmlStreamReader& reader) { if (reader.tokenType() != QXmlStreamReader::StartElement) return; - const QString href = reader.attributes().value(QLatin1String("href")).toString(); + const QString href = reader.attributes().value(u"href"_s).toString(); if (!copyImage(href)) return; // Handle inline images by substitution references. Insert a unique tag // enclosed by '|' and define it further down. Determine tag from the base //file name with number. QString tag = href; - int pos = tag.lastIndexOf(QLatin1Char('/')); + auto pos = tag.lastIndexOf(u'/'); if (pos != -1) tag.remove(0, pos + 1); - pos = tag.indexOf(QLatin1Char('.')); + pos = tag.indexOf(u'.'); if (pos != -1) tag.truncate(pos); tag += QString::number(m_inlineImages.size() + 1); @@ -1108,7 +1340,7 @@ void QtXmlToSphinx::handleRawTag(QXmlStreamReader& reader) { QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { - QString format = reader.attributes().value(QLatin1String("format")).toString(); + QString format = reader.attributes().value(u"format"_s).toString(); m_output << ".. raw:: " << format.toLower() << "\n\n"; } else if (token == QXmlStreamReader::Characters) { Indentation indent(m_output); @@ -1161,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); @@ -1178,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()); } @@ -1198,14 +1430,14 @@ void QtXmlToSphinx::handleAnchorTag(QXmlStreamReader& reader) QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::StartElement) { QString anchor; - if (reader.attributes().hasAttribute(QLatin1String("id"))) - anchor = reader.attributes().value(QLatin1String("id")).toString(); - else if (reader.attributes().hasAttribute(QLatin1String("name"))) - anchor = reader.attributes().value(QLatin1String("name")).toString(); + if (reader.attributes().hasAttribute(u"id"_s)) + anchor = reader.attributes().value(u"id"_s).toString(); + else if (reader.attributes().hasAttribute(u"name"_s)) + anchor = reader.attributes().value(u"name"_s).toString(); if (!anchor.isEmpty() && m_opened_anchor != anchor) { m_opened_anchor = anchor; if (!m_context.isEmpty()) - anchor.prepend(m_context + QLatin1Char('_')); + anchor.prepend(m_context + u'_'); m_output << rstLabel(anchor); } } else if (token == QXmlStreamReader::EndElement) { @@ -1224,7 +1456,7 @@ void QtXmlToSphinx::handleQuoteFileTag(QXmlStreamReader& reader) QXmlStreamReader::TokenType token = reader.tokenType(); if (token == QXmlStreamReader::Characters) { QString location = reader.text().toString(); - location.prepend(m_parameters.libSourceDir + QLatin1Char('/')); + location.prepend(m_parameters.libSourceDir + u'/'); QString errorMessage; QString code = readFromLocation(location, QString(), &errorMessage); if (!errorMessage.isEmpty()) @@ -1239,51 +1471,72 @@ void QtXmlToSphinx::handleQuoteFileTag(QXmlStreamReader& reader) } } +bool QtXmlToSphinx::Table::hasEmptyLeadingRow() const +{ + return !m_rows.isEmpty() && m_rows.constFirst().isEmpty(); +} + +bool QtXmlToSphinx::Table::hasEmptyTrailingRow() const +{ + return !m_rows.isEmpty() && m_rows.constLast().isEmpty(); +} + void QtXmlToSphinx::Table::normalize() { - if (m_normalized || isEmpty()) + if (m_normalized) + return; + + // Empty leading/trailing rows have been observed with nested tables + if (hasEmptyLeadingRow() || hasEmptyLeadingRow()) { + qWarning() << "QtXmlToSphinx: Table with leading/trailing empty columns found: " << *this; + while (hasEmptyTrailingRow()) + m_rows.pop_back(); + while (hasEmptyLeadingRow()) + m_rows.pop_front(); + } + + if (isEmpty()) return; //QDoc3 generates tables with wrong number of columns. We have to //check and if necessary, merge the last columns. - int maxCols = -1; - for (const auto &row : qAsConst(m_rows)) { - if (row.count() > maxCols) - maxCols = row.count(); + qsizetype maxCols = -1; + for (const auto &row : std::as_const(m_rows)) { + if (row.size() > maxCols) + maxCols = row.size(); } if (maxCols <= 0) return; // add col spans - for (int row = 0; row < m_rows.count(); ++row) { - for (int col = 0; col < m_rows.at(row).count(); ++col) { + for (qsizetype row = 0; row < m_rows.size(); ++row) { + for (qsizetype col = 0; col < m_rows.at(row).size(); ++col) { QtXmlToSphinx::TableCell& cell = m_rows[row][col]; bool mergeCols = (col >= maxCols); if (cell.colSpan > 0) { QtXmlToSphinx::TableCell newCell; newCell.colSpan = -1; - for (int i = 0, max = cell.colSpan-1; i < max; ++i) { + for (int i = 0, max = cell.colSpan-1; i < max; ++i) m_rows[row].insert(col + 1, newCell); - } cell.colSpan = 0; col++; } else if (mergeCols) { - m_rows[row][maxCols - 1].data += QLatin1Char(' ') + cell.data; + m_rows[row][maxCols - 1].data += u' ' + cell.data; } } } // row spans - const int numCols = m_rows.constFirst().count(); - for (int col = 0; col < numCols; ++col) { - for (int row = 0; row < m_rows.count(); ++row) { - if (col < m_rows[row].count()) { + const qsizetype numCols = m_rows.constFirst().size(); + for (qsizetype col = 0; col < numCols; ++col) { + for (qsizetype row = 0; row < m_rows.size(); ++row) { + if (col < m_rows[row].size()) { QtXmlToSphinx::TableCell& cell = m_rows[row][col]; if (cell.rowSpan > 0) { QtXmlToSphinx::TableCell newCell; newCell.rowSpan = -1; - int targetRow = row + 1; - const int targetEndRow = - std::min(targetRow + cell.rowSpan - 1, int(m_rows.count())); + qsizetype targetRow = row + 1; + const qsizetype targetEndRow = + std::min(targetRow + cell.rowSpan - 1, m_rows.size()); cell.rowSpan = 0; for ( ; targetRow < targetEndRow; ++targetRow) m_rows[targetRow].insert(col, newCell); @@ -1303,16 +1556,17 @@ void QtXmlToSphinx::Table::format(TextStream& s) const Q_ASSERT(isNormalized()); // calc width and height of each column and row - const int headerColumnCount = m_rows.constFirst().count(); - QList<int> colWidths(headerColumnCount, 0); - QList<int> rowHeights(m_rows.count(), 0); - for (int i = 0, maxI = m_rows.count(); i < maxI; ++i) { + const qsizetype headerColumnCount = m_rows.constFirst().size(); + QList<qsizetype> colWidths(headerColumnCount, 0); + QList<qsizetype> rowHeights(m_rows.size(), 0); + for (qsizetype i = 0, maxI = m_rows.size(); i < maxI; ++i) { const QtXmlToSphinx::TableRow& row = m_rows.at(i); - for (int j = 0, maxJ = std::min(row.count(), colWidths.size()); j < maxJ; ++j) { - const auto rowLines = QStringView{row[j].data}.split(QLatin1Char('\n')); // cache this would be a good idea + for (qsizetype j = 0, maxJ = std::min(row.size(), colWidths.size()); j < maxJ; ++j) { + // cache this would be a good idea + const auto rowLines = QStringView{row[j].data}.split(u'\n'); for (const auto &str : rowLines) - colWidths[j] = std::max(colWidths[j], int(str.size())); - rowHeights[i] = std::max(rowHeights[i], int(rowLines.size())); + colWidths[j] = std::max(colWidths[j], str.size()); + rowHeights[i] = std::max(rowHeights[i], rowLines.size()); } } @@ -1320,44 +1574,41 @@ void QtXmlToSphinx::Table::format(TextStream& s) const return; // empty table (table with empty cells) // create a horizontal line to be used later. - QString horizontalLine = QLatin1String("+"); - for (int i = 0, max = colWidths.count(); i < max; ++i) { - horizontalLine += QString(colWidths.at(i), QLatin1Char('-')); - horizontalLine += QLatin1Char('+'); - } + QString horizontalLine = u"+"_s; + for (auto colWidth : colWidths) + horizontalLine += QString(colWidth, u'-') + u'+'; // write table rows - for (int i = 0, maxI = m_rows.count(); i < maxI; ++i) { // for each row + for (qsizetype i = 0, maxI = m_rows.size(); i < maxI; ++i) { // for each row const QtXmlToSphinx::TableRow& row = m_rows.at(i); // print line s << '+'; - for (int col = 0; col < headerColumnCount; ++col) { - char c; - if (col >= row.length() || row[col].rowSpan == -1) + for (qsizetype col = 0; col < headerColumnCount; ++col) { + char c = '-'; + if (col >= row.size() || row[col].rowSpan == -1) c = ' '; else if (i == 1 && hasHeader()) c = '='; - else - c = '-'; s << Pad(c, colWidths.at(col)) << '+'; } s << '\n'; // Print the table cells - for (int rowLine = 0; rowLine < rowHeights[i]; ++rowLine) { // for each line in a row - int j = 0; - for (int maxJ = std::min(int(row.count()), headerColumnCount); j < maxJ; ++j) { // for each column + for (qsizetype rowLine = 0; rowLine < rowHeights.at(i); ++rowLine) { // for each line in a row + qsizetype j = 0; + for (qsizetype maxJ = std::min(row.size(), headerColumnCount); j < maxJ; ++j) { // for each column const QtXmlToSphinx::TableCell& cell = row[j]; - const auto rowLines = QStringView{cell.data}.split(QLatin1Char('\n')); // FIXME: Cache this!!! + // FIXME: Cache this!!! + const auto rowLines = QStringView{cell.data}.split(u'\n'); if (!j || !cell.colSpan) s << '|'; else s << ' '; - const int width = colWidths.at(j); - if (rowLine < rowLines.count()) + const auto width = int(colWidths.at(j)); + if (rowLine < rowLines.size()) s << AlignedField(rowLines.at(rowLine), width); else s << Pad(' ', width); @@ -1370,9 +1621,31 @@ void QtXmlToSphinx::Table::format(TextStream& s) const s << horizontalLine << "\n\n"; } +void QtXmlToSphinx::Table::formatDebug(QDebug &debug) const +{ + const auto rowCount = m_rows.size(); + debug << "Table(" <<rowCount << " rows"; + if (m_hasHeader) + debug << ", [header]"; + if (m_normalized) + debug << ", [normalized]"; + for (qsizetype r = 0; r < rowCount; ++r) { + const auto &row = m_rows.at(r); + const auto &colCount = row.size(); + debug << ", row " << r << " [" << colCount << "]={"; + for (qsizetype c = 0; c < colCount; ++c) { + if (c > 0) + debug << ", "; + debug << row.at(c); + } + debug << '}'; + } + debug << ')'; +} + void QtXmlToSphinx::stripPythonQualifiers(QString *s) { - const int lastSep = s->lastIndexOf(QLatin1Char('.')); + const int lastSep = s->lastIndexOf(u'.'); if (lastSep != -1) s->remove(0, lastSep + 1); } |