QTextMarkdownWriter: Avoid omitting or misplacing ending indicators

If we need to word-wrap a paragraph after a long formatted span,
write any ending markers before the newline (amends
280d679c556ab8ead4748a627d7cd4c1950027fb ).

Break before a fragment if the whole thing is past the column limit;
in that case, write out any ending format markers before the newline.

And now we have test coverage: prepend characters one-at-a-time to a
line that already has a two-word formatted span at the end, and watch
it successively break after the span, in the middle, and then before,
while never putting a newline before the ending markers or failing to
write them.

Fixes: QTBUG-116927
Change-Id: I140e10d19a491cb599bf7ecf8514af866b5383f3
Pick-to: 6.6 6.5
Reviewed-by: Axel Spoerl <axel.spoerl@qt.io>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
(cherry picked from commit 908fc2e72b34073dc42ce0f2d6f7cc5adc9651d9)
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
This commit is contained in:
Shawn Rutledge 2024-02-06 13:54:00 -07:00
parent 09236abe2d
commit 183ee5c177
2 changed files with 104 additions and 3 deletions

View File

@ -482,6 +482,7 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
bool italic = false;
bool underline = false;
bool strikeOut = false;
bool endingMarkers = false;
QString backticks(qtmw_Backtick);
for (QTextBlock::Iterator frag = block.begin(); !frag.atEnd(); ++frag) {
missedBlankCodeBlockLine = false;
@ -549,19 +550,27 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
if (startsOrEndsWithBacktick)
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
@ -569,6 +578,8 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
// That will have to do.
markers += u'_';
underline = fontInfo.underline();
if (!underline)
endingMarkers = true;
}
}
}
@ -578,7 +589,8 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
bool breakingLine = false;
while (i < fragLen) {
if (col >= ColumnLimit) {
m_stream << qtmw_Newline << wrapIndentString;
m_stream << markers << qtmw_Newline << wrapIndentString;
markers.clear();
col = m_wrappedLineIndent;
while (i < fragLen && fragmentText[i].isSpace())
++i;
@ -588,6 +600,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;
@ -611,7 +630,7 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
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;

View File

@ -36,6 +36,8 @@ private slots:
void testWriteNestedNumericLists();
void testWriteNumericListWithStart();
void testWriteTable();
void charFormatWrapping_data();
void charFormatWrapping();
void rewriteDocument_data();
void rewriteDocument();
void fromHtml_data();
@ -525,6 +527,86 @@ void tst_QTextMarkdownWriter::testWriteTable()
QCOMPARE(md, expected);
}
void tst_QTextMarkdownWriter::charFormatWrapping_data()
{
QTest::addColumn<QTextFormat::Property>("property");
QTest::addColumn<QVariant>("propertyValue");
QTest::addColumn<QString>("followingText");
QTest::addColumn<QString>("expectedIndicator");
const QString spaced = " after";
const QString unspaced = ", and some more after";
QTest::newRow("FontFixedPitch-spaced")
<< QTextFormat::FontFixedPitch << QVariant(true) << spaced << "`";
QTest::newRow("FontFixedPitch-unspaced")
<< QTextFormat::FontFixedPitch << QVariant(true) << unspaced << "`";
QTest::newRow("FontItalic")
<< QTextFormat::FontItalic << QVariant(true) << spaced << "*";
QTest::newRow("FontUnderline")
<< QTextFormat::FontUnderline << QVariant(true) << spaced << "_";
QTest::newRow("FontStrikeOut")
<< QTextFormat::FontStrikeOut << QVariant(true) << spaced << "~~";
QTest::newRow("FontWeight-spaced")
<< QTextFormat::FontWeight << QVariant(700) << spaced << "**";
QTest::newRow("FontWeight-unspaced")
<< QTextFormat::FontWeight << QVariant(700) << unspaced << "**";
}
void tst_QTextMarkdownWriter::charFormatWrapping() // QTBUG-116927
{
QFETCH(QTextFormat::Property, property);
QFETCH(QVariant, propertyValue);
QFETCH(QString, expectedIndicator);
QFETCH(QString, followingText);
const QString newLine("\n");
QTextCursor cursor(document);
cursor.insertText("around sixty-four characters to go before some formatted words ");
QTextCharFormat fmt;
fmt.setProperty(property, propertyValue);
cursor.setCharFormat(fmt);
cursor.insertText("formatted text");
cursor.setCharFormat({});
cursor.insertText(followingText);
qsizetype lastNewLineIndex = 100;
for (int push = 0; push < 10; ++push) {
if (push > 0) {
cursor.movePosition(QTextCursor::StartOfBlock);
cursor.insertText("a");
}
const QString output = documentToUnixMarkdown().trimmed(); // get rid of trailing newlines
const auto nlIdx = output.indexOf(newLine);
qCDebug(lcTests) << "push" << push << ":" << output << "newline @" << nlIdx;
// we're always wrapping in this test: expect to find a newline
QCOMPARE_GT(nlIdx, 70);
// don't expect the newline to be more than one character to the right of where we found it last time
// i.e. if we already started breaking in the middle: "`formatted\ntext`",
// then we would not expect that prepending one more character would make it go
// back to breaking afterwards: "`formatted text`\n" (because then the line becomes longer than necessary)
QCOMPARE_LE(nlIdx, lastNewLineIndex + 1);
lastNewLineIndex = nlIdx;
const QString nextChars = output.sliced(nlIdx + newLine.size(), expectedIndicator.size());
const auto startingIndicatorIdx = output.indexOf(expectedIndicator);
// the starting indicator always exists, except in case of font problems on some CI platforms
if (startingIndicatorIdx <= 0)
QSKIP("starting indicator not found, probably due to platform font problems (QTBUG-103484 etc.)");
const auto endingIndicatorIdx = output.indexOf(expectedIndicator, startingIndicatorIdx + 5);
qCDebug(lcTests) << "next chars past newline" << nextChars
<< "indicators @" << startingIndicatorIdx << endingIndicatorIdx;
// the closing indicator must exist
QCOMPARE_GT(endingIndicatorIdx, startingIndicatorIdx);
// don't start a new line with an ending indicator:
// we can have "**formatted\ntext**" or "**formatted text**\n" or "\n**formatted text**"
// but not "**formatted text\n**"
if (startingIndicatorIdx < nlIdx)
QCOMPARE_NE(nextChars, expectedIndicator);
}
}
void tst_QTextMarkdownWriter::rewriteDocument_data()
{
QTest::addColumn<QString>("inputFile");
@ -569,7 +651,7 @@ void tst_QTextMarkdownWriter::fromHtml_data()
QTest::newRow("long URL") <<
"<span style=\"font-style:italic;\">https://www.example.com/dir/subdir/subsubdir/subsubsubdir/subsubsubsubdir/subsubsubsubsubdir/</span>" <<
"*https://www.example.com/dir/subdir/subsubdir/subsubsubdir/subsubsubsubdir/subsubsubsubsubdir/*\n\n";
"\n*https://www.example.com/dir/subdir/subsubdir/subsubsubdir/subsubsubsubdir/subsubsubsubsubdir/*\n\n";
QTest::newRow("non-emphasis inline asterisk") << "3 * 4" << "3 * 4\n\n";
QTest::newRow("arithmetic") << "(2 * a * x + b)^2 = b^2 - 4 * a * c" << "(2 * a * x + b)^2 = b^2 - 4 * a * c\n\n";
QTest::newRow("escaped asterisk after newline") <<