aboutsummaryrefslogtreecommitdiffstats
path: root/sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp')
-rw-r--r--sources/shiboken6/generator/qtdoc/qtxmltosphinx.cpp274
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';