summaryrefslogtreecommitdiffstats
path: root/src/gui/text/qtextmarkdownwriter.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui/text/qtextmarkdownwriter.cpp')
-rw-r--r--src/gui/text/qtextmarkdownwriter.cpp344
1 files changed, 234 insertions, 110 deletions
diff --git a/src/gui/text/qtextmarkdownwriter.cpp b/src/gui/text/qtextmarkdownwriter.cpp
index 9f651a04fd..361158e722 100644
--- a/src/gui/text/qtextmarkdownwriter.cpp
+++ b/src/gui/text/qtextmarkdownwriter.cpp
@@ -10,7 +10,9 @@
#include "qtexttable.h"
#include "qtextcursor.h"
#include "qtextimagehandler_p.h"
+#include "qtextmarkdownimporter_p.h"
#include "qloggingcategory.h"
+#include <QtCore/QRegularExpression>
#if QT_CONFIG(itemmodel)
#include "qabstractitemmodel.h"
#endif
@@ -21,15 +23,15 @@ using namespace Qt::StringLiterals;
Q_LOGGING_CATEGORY(lcMDW, "qt.text.markdown.writer")
-static const QChar Space = u' ';
-static const QChar Tab = u'\t';
-static const QChar Newline = u'\n';
-static const QChar CarriageReturn = u'\r';
-static const QChar LineBreak = u'\x2028';
-static const QChar DoubleQuote = u'"';
-static const QChar Backtick = u'`';
-static const QChar Backslash = u'\\';
-static const QChar Period = u'.';
+static const QChar qtmw_Space = u' ';
+static const QChar qtmw_Tab = u'\t';
+static const QChar qtmw_Newline = u'\n';
+static const QChar qtmw_CarriageReturn = u'\r';
+static const QChar qtmw_LineBreak = u'\x2028';
+static const QChar qtmw_DoubleQuote = u'"';
+static const QChar qtmw_Backtick = u'`';
+static const QChar qtmw_Backslash = u'\\';
+static const QChar qtmw_Period = u'.';
QTextMarkdownWriter::QTextMarkdownWriter(QTextStream &stream, QTextDocument::MarkdownFeatures features)
: m_stream(stream), m_features(features)
@@ -38,6 +40,7 @@ QTextMarkdownWriter::QTextMarkdownWriter(QTextStream &stream, QTextDocument::Mar
bool QTextMarkdownWriter::writeAll(const QTextDocument *document)
{
+ writeFrontMatter(document->metaInformation(QTextDocument::FrontMatter));
writeFrame(document->rootFrame());
return true;
}
@@ -47,20 +50,20 @@ void QTextMarkdownWriter::writeTable(const QAbstractItemModel *table)
{
QList<int> tableColumnWidths(table->columnCount());
for (int col = 0; col < table->columnCount(); ++col) {
- tableColumnWidths[col] = table->headerData(col, Qt::Horizontal).toString().length();
+ tableColumnWidths[col] = table->headerData(col, Qt::Horizontal).toString().size();
for (int row = 0; row < table->rowCount(); ++row) {
tableColumnWidths[col] = qMax(tableColumnWidths[col],
- table->data(table->index(row, col)).toString().length());
+ table->data(table->index(row, col)).toString().size());
}
}
// write the header and separator
for (int col = 0; col < table->columnCount(); ++col) {
QString s = table->headerData(col, Qt::Horizontal).toString();
- m_stream << "|" << s << QString(tableColumnWidths[col] - s.length(), Space);
+ m_stream << '|' << s << QString(tableColumnWidths[col] - s.size(), qtmw_Space);
}
m_stream << "|" << Qt::endl;
- for (int col = 0; col < tableColumnWidths.length(); ++col)
+ for (int col = 0; col < tableColumnWidths.size(); ++col)
m_stream << '|' << QString(tableColumnWidths[col], u'-');
m_stream << '|'<< Qt::endl;
@@ -68,7 +71,7 @@ void QTextMarkdownWriter::writeTable(const QAbstractItemModel *table)
for (int row = 0; row < table->rowCount(); ++row) {
for (int col = 0; col < table->columnCount(); ++col) {
QString s = table->data(table->index(row, col)).toString();
- m_stream << "|" << s << QString(tableColumnWidths[col] - s.length(), Space);
+ m_stream << '|' << s << QString(tableColumnWidths[col] - s.size(), qtmw_Space);
}
m_stream << '|'<< Qt::endl;
}
@@ -76,6 +79,19 @@ void QTextMarkdownWriter::writeTable(const QAbstractItemModel *table)
}
#endif
+void QTextMarkdownWriter::writeFrontMatter(const QString &fm)
+{
+ const bool featureEnabled = m_features.testFlag(
+ static_cast<QTextDocument::MarkdownFeature>(QTextMarkdownImporter::FeatureFrontMatter));
+ qCDebug(lcMDW) << "writing FrontMatter?" << featureEnabled << "size" << fm.size();
+ if (fm.isEmpty() || !featureEnabled)
+ return;
+ m_stream << "---\n"_L1 << fm;
+ if (!fm.endsWith(qtmw_Newline))
+ m_stream << qtmw_Newline;
+ m_stream << "---\n"_L1;
+}
+
void QTextMarkdownWriter::writeFrame(const QTextFrame *frame)
{
Q_ASSERT(frame);
@@ -95,7 +111,7 @@ void QTextMarkdownWriter::writeFrame(const QTextFrame *frame)
while (it != cell.end()) {
QTextBlock block = it.currentBlock();
if (block.isValid())
- cellTextLen += block.text().length();
+ cellTextLen += block.text().size();
++it;
}
if (cell.columnSpan() == 1 && tableColumnWidths[col] < cellTextLen)
@@ -112,17 +128,22 @@ void QTextMarkdownWriter::writeFrame(const QTextFrame *frame)
// suppress needless blank lines, when there will be a big change in block format
bool nextIsDifferent = false;
bool ending = false;
+ int blockQuoteIndent = 0;
+ int nextBlockQuoteIndent = 0;
{
QTextFrame::iterator next = iterator;
++next;
+ QTextBlockFormat format = iterator.currentBlock().blockFormat();
+ QTextBlockFormat nextFormat = next.currentBlock().blockFormat();
+ blockQuoteIndent = format.intProperty(QTextFormat::BlockQuoteLevel);
+ nextBlockQuoteIndent = nextFormat.intProperty(QTextFormat::BlockQuoteLevel);
if (next.atEnd()) {
nextIsDifferent = true;
ending = true;
} else {
- QTextBlockFormat format = iterator.currentBlock().blockFormat();
- QTextBlockFormat nextFormat = next.currentBlock().blockFormat();
if (nextFormat.indent() != format.indent() ||
- nextFormat.property(QTextFormat::BlockCodeLanguage) != format.property(QTextFormat::BlockCodeLanguage))
+ nextFormat.property(QTextFormat::BlockCodeLanguage) !=
+ format.property(QTextFormat::BlockCodeLanguage))
nextIsDifferent = true;
}
}
@@ -130,17 +151,19 @@ void QTextMarkdownWriter::writeFrame(const QTextFrame *frame)
QTextTableCell cell = table->cellAt(block.position());
if (tableRow < cell.row()) {
if (tableRow == 0) {
- m_stream << Newline;
- for (int col = 0; col < tableColumnWidths.length(); ++col)
+ m_stream << qtmw_Newline;
+ for (int col = 0; col < tableColumnWidths.size(); ++col)
m_stream << '|' << QString(tableColumnWidths[col], u'-');
m_stream << '|';
}
- m_stream << Newline << "|";
+ m_stream << qtmw_Newline << '|';
tableRow = cell.row();
}
} else if (!block.textList()) {
- if (lastWasList)
- m_stream << Newline;
+ if (lastWasList) {
+ m_stream << qtmw_Newline;
+ m_linePrefixWritten = false;
+ }
}
int endingCol = writeBlock(block, !table, table && tableRow == 0,
nextIsDifferent && !block.textList());
@@ -152,20 +175,28 @@ void QTextMarkdownWriter::writeFrame(const QTextFrame *frame)
for (int col = cell.column(); col < spanEndCol; ++col)
paddingLen += tableColumnWidths[col];
if (paddingLen > 0)
- m_stream << QString(paddingLen, Space);
+ m_stream << QString(paddingLen, qtmw_Space);
for (int col = cell.column(); col < spanEndCol; ++col)
m_stream << "|";
} else if (m_fencedCodeBlock && ending) {
- m_stream << Newline << m_linePrefix << QString(m_wrappedLineIndent, Space)
- << m_codeBlockFence << Newline << Newline;
+ m_stream << qtmw_Newline << m_linePrefix << QString(m_wrappedLineIndent, qtmw_Space)
+ << m_codeBlockFence << qtmw_Newline << qtmw_Newline;
m_codeBlockFence.clear();
} else if (m_indentedCodeBlock && nextIsDifferent) {
- m_stream << Newline << Newline;
+ m_stream << qtmw_Newline << qtmw_Newline;
} else if (endingCol > 0) {
if (block.textList() || block.blockFormat().hasProperty(QTextFormat::BlockCodeLanguage)) {
- m_stream << Newline;
+ m_stream << qtmw_Newline;
+ if (block.textList()) {
+ m_stream << m_linePrefix;
+ m_linePrefixWritten = true;
+ }
} else {
- m_stream << Newline << Newline;
+ m_stream << qtmw_Newline;
+ if (nextBlockQuoteIndent < blockQuoteIndent)
+ setLinePrefixForBlockQuote(nextBlockQuoteIndent);
+ m_stream << m_linePrefix;
+ m_stream << qtmw_Newline;
m_doubleNewlineWritten = true;
}
}
@@ -175,7 +206,7 @@ void QTextMarkdownWriter::writeFrame(const QTextFrame *frame)
++iterator;
}
if (table) {
- m_stream << Newline << Newline;
+ m_stream << qtmw_Newline << qtmw_Newline;
m_doubleNewlineWritten = true;
}
m_listInfo.clear();
@@ -211,18 +242,28 @@ QTextMarkdownWriter::ListInfo QTextMarkdownWriter::listInfo(QTextList *list)
return m_listInfo.value(list);
}
+void QTextMarkdownWriter::setLinePrefixForBlockQuote(int level)
+{
+ m_linePrefix.clear();
+ if (level > 0) {
+ m_linePrefix.reserve(level * 2);
+ for (int i = 0; i < level; ++i)
+ m_linePrefix += u"> ";
+ }
+}
+
static int nearestWordWrapIndex(const QString &s, int before)
{
- before = qMin(before, s.length());
+ before = qMin(before, s.size());
int fragBegin = qMax(before - 15, 0);
if (lcMDW().isDebugEnabled()) {
QString frag = s.mid(fragBegin, 30);
qCDebug(lcMDW) << frag << before;
- qCDebug(lcMDW) << QString(before - fragBegin, Period) + u'<';
+ qCDebug(lcMDW) << QString(before - fragBegin, qtmw_Period) + u'<';
}
for (int i = before - 1; i >= 0; --i) {
if (s.at(i).isSpace()) {
- qCDebug(lcMDW) << QString(i - fragBegin, Period) + u'^' << i;
+ qCDebug(lcMDW) << QString(i - fragBegin, qtmw_Period) + u'^' << i;
return i;
}
}
@@ -232,10 +273,10 @@ static int nearestWordWrapIndex(const QString &s, int before)
static int adjacentBackticksCount(const QString &s)
{
- int start = -1, len = s.length();
+ int start = -1, len = s.size();
int ret = 0;
for (int i = 0; i < len; ++i) {
- if (s.at(i) == Backtick) {
+ if (s.at(i) == qtmw_Backtick) {
if (start < 0)
start = i;
} else if (start >= 0) {
@@ -243,20 +284,58 @@ static int adjacentBackticksCount(const QString &s)
start = -1;
}
}
- if (s.at(len - 1) == Backtick)
+ if (s.at(len - 1) == qtmw_Backtick)
ret = qMax(ret, len - start);
return ret;
}
+/*! \internal
+ Escape anything at the beginning of a line of markdown that would be
+ misinterpreted by a markdown parser, including any period that follows a
+ number (to avoid misinterpretation as a numbered list item).
+ https://spec.commonmark.org/0.31.2/#backslash-escapes
+*/
static void maybeEscapeFirstChar(QString &s)
{
+ static const QRegularExpression numericListRe(uR"(\d+([\.)])\s)"_s);
+ static const QLatin1StringView specialFirstCharacters("#*+-");
+
QString sTrimmed = s.trimmed();
if (sTrimmed.isEmpty())
return;
- char firstChar = sTrimmed.at(0).toLatin1();
- if (firstChar == '*' || firstChar == '+' || firstChar == '-') {
- int i = s.indexOf(QLatin1Char(firstChar));
+ QChar firstChar = sTrimmed.at(0);
+ if (specialFirstCharacters.contains(firstChar)) {
+ int i = s.indexOf(firstChar); // == 0 unless s got trimmed
s.insert(i, u'\\');
+ } else {
+ auto match = numericListRe.match(s, 0, QRegularExpression::NormalMatch,
+ QRegularExpression::AnchorAtOffsetMatchOption);
+ if (match.hasMatch())
+ s.insert(match.capturedStart(1), qtmw_Backslash);
+ }
+}
+
+/*! \internal
+ Escape all backslashes. Then escape any special character that stands
+ alone or prefixes a "word", including the \c < that starts an HTML tag.
+ https://spec.commonmark.org/0.31.2/#backslash-escapes
+*/
+static void escapeSpecialCharacters(QString &s)
+{
+ static const QRegularExpression spaceRe(uR"(\s+)"_s);
+ static const QRegularExpression specialRe(uR"([<!*[`&]+[/\w])"_s);
+
+ s.replace("\\"_L1, "\\\\"_L1);
+
+ int i = 0;
+ while (i >= 0) {
+ if (int j = s.indexOf(specialRe, i); j >= 0) {
+ s.insert(j, qtmw_Backslash);
+ i = j + 3;
+ }
+ i = s.indexOf(spaceRe, i);
+ if (i >= 0)
+ ++i; // past the whitespace, if found
}
}
@@ -270,14 +349,14 @@ static LineEndPositions findLineEnd(const QChar *begin, const QChar *end)
LineEndPositions result{ end, end };
while (begin < end) {
- if (*begin == Newline) {
+ if (*begin == qtmw_Newline) {
result.lineEnd = begin;
result.nextLineBegin = begin + 1;
break;
- } else if (*begin == CarriageReturn) {
+ } else if (*begin == qtmw_CarriageReturn) {
result.lineEnd = begin;
result.nextLineBegin = begin + 1;
- if (((begin + 1) < end) && begin[1] == Newline)
+ if (((begin + 1) < end) && begin[1] == qtmw_Newline)
++result.nextLineBegin;
break;
}
@@ -291,7 +370,7 @@ static LineEndPositions findLineEnd(const QChar *begin, const QChar *end)
static bool isBlankLine(const QChar *begin, const QChar *end)
{
while (begin < end) {
- if (*begin != Space && *begin != Tab)
+ if (*begin != qtmw_Space && *begin != qtmw_Tab)
return false;
++begin;
}
@@ -302,7 +381,7 @@ static QString createLinkTitle(const QString &title)
{
QString result;
result.reserve(title.size() + 2);
- result += DoubleQuote;
+ result += qtmw_DoubleQuote;
const QChar *data = title.data();
const QChar *end = data + title.size();
@@ -312,8 +391,8 @@ static QString createLinkTitle(const QString &title)
if (!isBlankLine(data, lineEndPositions.lineEnd)) {
while (data < lineEndPositions.nextLineBegin) {
- if (*data == DoubleQuote)
- result += Backslash;
+ if (*data == qtmw_DoubleQuote)
+ result += qtmw_Backslash;
result += *data;
++data;
}
@@ -322,7 +401,7 @@ static QString createLinkTitle(const QString &title)
data = lineEndPositions.nextLineBegin;
}
- result += DoubleQuote;
+ result += qtmw_DoubleQuote;
return result;
}
@@ -334,17 +413,29 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
QTextBlockFormat blockFmt = block.blockFormat();
bool missedBlankCodeBlockLine = false;
const bool codeBlock = blockFmt.hasProperty(QTextFormat::BlockCodeFence) ||
- blockFmt.stringProperty(QTextFormat::BlockCodeLanguage).length() > 0 ||
+ blockFmt.stringProperty(QTextFormat::BlockCodeLanguage).size() > 0 ||
blockFmt.nonBreakableLines();
+ const int blockQuoteLevel = blockFmt.intProperty(QTextFormat::BlockQuoteLevel);
if (m_fencedCodeBlock && !codeBlock) {
- m_stream << m_linePrefix << m_codeBlockFence << Newline;
+ m_stream << m_linePrefix << m_codeBlockFence << qtmw_Newline;
m_fencedCodeBlock = false;
m_codeBlockFence.clear();
+ m_linePrefixWritten = m_linePrefix.size() > 0;
+ }
+ m_linePrefix.clear();
+ if (!blockFmt.headingLevel() && blockQuoteLevel > 0) {
+ setLinePrefixForBlockQuote(blockQuoteLevel);
+ if (!m_linePrefixWritten) {
+ m_stream << m_linePrefix;
+ m_linePrefixWritten = true;
+ }
}
if (block.textList()) { // it's a list-item
auto fmt = block.textList()->format();
const int listLevel = fmt.indent();
- const int number = block.textList()->itemNumber(block) + 1;
+ // Negative numbers don't start a list in Markdown, so ignore them.
+ const int start = fmt.start() >= 0 ? fmt.start() : 1;
+ const int number = block.textList()->itemNumber(block) + start;
QByteArray bullet = " ";
bool numeric = false;
switch (fmt.style()) {
@@ -383,19 +474,19 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
int indentFirstLine = (listLevel - 1) * (numeric ? 4 : 2);
m_wrappedLineIndent += indentFirstLine;
if (m_lastListIndent != listLevel && !m_doubleNewlineWritten && listInfo(block.textList()).loose)
- m_stream << Newline;
+ m_stream << qtmw_Newline;
m_lastListIndent = listLevel;
- QString prefix(indentFirstLine, Space);
+ QString prefix(indentFirstLine, qtmw_Space);
if (numeric) {
QString suffix = fmt.numberSuffix();
if (suffix.isEmpty())
- suffix = QString(Period);
- QString numberStr = QString::number(number) + suffix + Space;
- if (numberStr.length() == 3)
- numberStr += Space;
+ suffix = QString(qtmw_Period);
+ QString numberStr = QString::number(number) + suffix + qtmw_Space;
+ if (numberStr.size() == 3)
+ numberStr += qtmw_Space;
prefix += numberStr;
} else {
- prefix += QLatin1StringView(bullet) + Space;
+ prefix += QLatin1StringView(bullet) + qtmw_Space;
}
m_stream << prefix;
} else if (blockFmt.hasProperty(QTextFormat::BlockTrailingHorizontalRulerWidth)) {
@@ -412,60 +503,63 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
fenceChar = "`"_L1;
m_codeBlockFence = QString(3, fenceChar.at(0));
if (blockFmt.hasProperty(QTextFormat::BlockIndent))
- m_codeBlockFence = QString(m_wrappedLineIndent, Space) + m_codeBlockFence;
+ m_codeBlockFence = QString(m_wrappedLineIndent, qtmw_Space) + m_codeBlockFence;
// A block quote can contain an indented code block, but not vice-versa.
- m_stream << m_linePrefix << m_codeBlockFence
- << blockFmt.stringProperty(QTextFormat::BlockCodeLanguage) << Newline;
+ m_stream << m_codeBlockFence << blockFmt.stringProperty(QTextFormat::BlockCodeLanguage)
+ << qtmw_Newline << m_linePrefix;
m_fencedCodeBlock = true;
}
wrap = false;
} else if (!blockFmt.indent()) {
m_wrappedLineIndent = 0;
- m_linePrefix.clear();
- if (blockFmt.hasProperty(QTextFormat::BlockQuoteLevel)) {
- int level = blockFmt.intProperty(QTextFormat::BlockQuoteLevel);
- QString quoteMarker = QStringLiteral("> ");
- m_linePrefix.reserve(level * 2);
- for (int i = 0; i < level; ++i)
- m_linePrefix += quoteMarker;
- }
if (blockFmt.hasProperty(QTextFormat::BlockCodeLanguage)) {
// A block quote can contain an indented code block, but not vice-versa.
- m_linePrefix += QString(4, Space);
+ m_linePrefix += QString(4, qtmw_Space);
m_indentedCodeBlock = true;
}
+ if (!m_linePrefixWritten) {
+ m_stream << m_linePrefix;
+ m_linePrefixWritten = true;
+ }
}
- if (blockFmt.headingLevel())
+ if (blockFmt.headingLevel()) {
m_stream << QByteArray(blockFmt.headingLevel(), '#') << ' ';
- else
- m_stream << m_linePrefix;
+ wrap = false;
+ }
- QString wrapIndentString = m_linePrefix + QString(m_wrappedLineIndent, Space);
+ QString wrapIndentString = m_linePrefix + QString(m_wrappedLineIndent, qtmw_Space);
// It would be convenient if QTextStream had a lineCharPos() accessor,
// to keep track of how many characters (not bytes) have been written on the current line,
// but it doesn't. So we have to keep track with this col variable.
- int col = wrapIndentString.length();
+ int col = wrapIndentString.size();
bool mono = false;
bool startsOrEndsWithBacktick = false;
bool bold = false;
bool italic = false;
bool underline = false;
bool strikeOut = false;
- QString backticks(Backtick);
+ bool endingMarkers = false;
+ QString backticks(qtmw_Backtick);
for (QTextBlock::Iterator frag = block.begin(); !frag.atEnd(); ++frag) {
missedBlankCodeBlockLine = false;
QString fragmentText = frag.fragment().text();
- while (fragmentText.endsWith(Newline))
+ while (fragmentText.endsWith(qtmw_Newline))
fragmentText.chop(1);
+ if (!(m_fencedCodeBlock || m_indentedCodeBlock)) {
+ escapeSpecialCharacters(fragmentText);
+ maybeEscapeFirstChar(fragmentText);
+ }
if (block.textList()) { // <li>first line</br>continuation</li>
- QString newlineIndent = QString(Newline) + QString(m_wrappedLineIndent, Space);
- fragmentText.replace(QString(LineBreak), newlineIndent);
+ QString newlineIndent =
+ QString(qtmw_Newline) + QString(m_wrappedLineIndent, qtmw_Space);
+ fragmentText.replace(QString(qtmw_LineBreak), newlineIndent);
} else if (blockFmt.indent() > 0) { // <li>first line<p>continuation</p></li>
- m_stream << QString(m_wrappedLineIndent, Space);
+ m_stream << QString(m_wrappedLineIndent, qtmw_Space);
} else {
- fragmentText.replace(LineBreak, Newline);
+ fragmentText.replace(qtmw_LineBreak, qtmw_Newline);
}
- startsOrEndsWithBacktick |= fragmentText.startsWith(Backtick) || fragmentText.endsWith(Backtick);
+ startsOrEndsWithBacktick |=
+ fragmentText.startsWith(qtmw_Backtick) || fragmentText.endsWith(qtmw_Backtick);
QTextCharFormat fmt = frag.fragment().charFormat();
if (fmt.isImageFormat()) {
QTextImageFormat ifmt = fmt.toImageFormat();
@@ -475,28 +569,34 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
QString s = "!["_L1 + desc + "]("_L1 + ifmt.name();
QString title = ifmt.stringProperty(QTextFormat::ImageTitle);
if (!title.isEmpty())
- s += Space + DoubleQuote + title + DoubleQuote;
+ s += qtmw_Space + qtmw_DoubleQuote + title + qtmw_DoubleQuote;
s += u')';
- if (wrap && col + s.length() > ColumnLimit) {
- m_stream << Newline << wrapIndentString;
+ if (wrap && col + s.size() > ColumnLimit) {
+ m_stream << qtmw_Newline << wrapIndentString;
col = m_wrappedLineIndent;
}
m_stream << s;
- col += s.length();
+ col += s.size();
} else if (fmt.hasProperty(QTextFormat::AnchorHref)) {
- QString s = u'[' + fragmentText + "]("_L1 +
- fmt.property(QTextFormat::AnchorHref).toString();
- if (fmt.hasProperty(QTextFormat::TextToolTip)) {
- s += Space;
- s += createLinkTitle(fmt.property(QTextFormat::TextToolTip).toString());
+ const auto href = fmt.property(QTextFormat::AnchorHref).toString();
+ const bool hasToolTip = fmt.hasProperty(QTextFormat::TextToolTip);
+ QString s;
+ if (!hasToolTip && href == fragmentText && !QUrl(href, QUrl::StrictMode).scheme().isEmpty()) {
+ s = u'<' + href + u'>';
+ } else {
+ s = u'[' + fragmentText + "]("_L1 + href;
+ if (hasToolTip) {
+ s += qtmw_Space;
+ s += createLinkTitle(fmt.property(QTextFormat::TextToolTip).toString());
+ }
+ s += u')';
}
- s += u')';
- if (wrap && col + s.length() > ColumnLimit) {
- m_stream << Newline << wrapIndentString;
+ if (wrap && col + s.size() > ColumnLimit) {
+ m_stream << qtmw_Newline << wrapIndentString;
col = m_wrappedLineIndent;
}
m_stream << s;
- col += s.length();
+ col += s.size();
} else {
QFontInfo fontInfo(fmt.font());
bool monoFrag = fontInfo.fixedPitch() || fmt.fontFixedPitch();
@@ -504,43 +604,55 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
if (!ignoreFormat) {
if (monoFrag != mono && !m_indentedCodeBlock && !m_fencedCodeBlock) {
if (monoFrag)
- backticks = QString(adjacentBackticksCount(fragmentText) + 1, Backtick);
+ backticks =
+ QString(adjacentBackticksCount(fragmentText) + 1, qtmw_Backtick);
markers += backticks;
if (startsOrEndsWithBacktick)
- markers += Space;
+ markers += qtmw_Space;
mono = monoFrag;
+ if (!mono)
+ endingMarkers = true;
}
if (!blockFmt.headingLevel() && !mono) {
if (fontInfo.bold() != bold) {
markers += "**"_L1;
bold = fontInfo.bold();
+ if (!bold)
+ endingMarkers = true;
}
if (fontInfo.italic() != italic) {
markers += u'*';
italic = fontInfo.italic();
+ if (!italic)
+ endingMarkers = true;
}
if (fontInfo.strikeOut() != strikeOut) {
markers += "~~"_L1;
strikeOut = fontInfo.strikeOut();
+ if (!strikeOut)
+ endingMarkers = true;
}
if (fontInfo.underline() != underline) {
- // Markdown doesn't support underline, but the parser will treat a single underline
- // the same as a single asterisk, and the marked fragment will be rendered in italics.
- // That will have to do.
+ // CommonMark specifies underline as another way to get emphasis (italics):
+ // https://spec.commonmark.org/0.31.2/#example-148
+ // but md4c allows us to distinguish them; so we support underlining (in GitHub dialect).
markers += u'_';
underline = fontInfo.underline();
+ if (!underline)
+ endingMarkers = true;
}
}
}
- if (wrap && col + markers.length() * 2 + fragmentText.length() > ColumnLimit) {
+ if (wrap && col + markers.size() * 2 + fragmentText.size() > ColumnLimit) {
int i = 0;
- int fragLen = fragmentText.length();
+ const int fragLen = fragmentText.size();
bool breakingLine = false;
while (i < fragLen) {
if (col >= ColumnLimit) {
- m_stream << Newline << wrapIndentString;
+ m_stream << markers << qtmw_Newline << wrapIndentString;
+ markers.clear();
col = m_wrappedLineIndent;
- while (fragmentText[i].isSpace())
+ while (i < fragLen && fragmentText[i].isSpace())
++i;
}
int j = i + ColumnLimit - col;
@@ -548,6 +660,13 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
int wi = nearestWordWrapIndex(fragmentText, j);
if (wi < 0) {
j = fragLen;
+ // can't break within the fragment: we need to break already _before_ it
+ if (endingMarkers) {
+ m_stream << markers;
+ markers.clear();
+ }
+ m_stream << qtmw_Newline << wrapIndentString;
+ col = m_wrappedLineIndent;
} else if (wi >= i) {
j = wi;
breakingLine = true;
@@ -559,28 +678,32 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
QString subfrag = fragmentText.mid(i, j - i);
if (!i) {
m_stream << markers;
- col += markers.length();
+ col += markers.size();
}
if (col == m_wrappedLineIndent)
maybeEscapeFirstChar(subfrag);
m_stream << subfrag;
if (breakingLine) {
- m_stream << Newline << wrapIndentString;
+ m_stream << qtmw_Newline << wrapIndentString;
col = m_wrappedLineIndent;
} else {
- col += subfrag.length();
+ col += subfrag.size();
}
i = j + 1;
- }
+ } // loop over fragment characters (we know we need to break somewhere)
} else {
+ if (!m_linePrefixWritten && col == wrapIndentString.size()) {
+ m_stream << m_linePrefix;
+ col += m_linePrefix.size();
+ }
m_stream << markers << fragmentText;
- col += markers.length() + fragmentText.length();
+ col += markers.size() + fragmentText.size();
}
}
}
if (mono) {
if (startsOrEndsWithBacktick) {
- m_stream << Space;
+ m_stream << qtmw_Space;
col += 1;
}
m_stream << backticks;
@@ -603,7 +726,8 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
col += 2;
}
if (missedBlankCodeBlockLine)
- m_stream << Newline;
+ m_stream << qtmw_Newline;
+ m_linePrefixWritten = false;
return col;
}