diff options
Diffstat (limited to 'src/gui/text/qtextmarkdownwriter.cpp')
-rw-r--r-- | src/gui/text/qtextmarkdownwriter.cpp | 330 |
1 files changed, 224 insertions, 106 deletions
diff --git a/src/gui/text/qtextmarkdownwriter.cpp b/src/gui/text/qtextmarkdownwriter.cpp index cda1f209ad..2e23dfcd94 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 @@ -19,17 +21,17 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -Q_LOGGING_CATEGORY(lcMDW, "qt.text.markdown.writer") +Q_STATIC_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,14 +569,14 @@ 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)) { const auto href = fmt.property(QTextFormat::AnchorHref).toString(); const bool hasToolTip = fmt.hasProperty(QTextFormat::TextToolTip); @@ -492,17 +586,17 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign } else { s = u'[' + fragmentText + "]("_L1 + href; if (hasToolTip) { - s += Space; + s += qtmw_Space; s += createLinkTitle(fmt.property(QTextFormat::TextToolTip).toString()); } 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(); @@ -510,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; @@ -554,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; @@ -565,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; @@ -609,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; } |