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:
parent
09236abe2d
commit
183ee5c177
@ -482,6 +482,7 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
|
|||||||
bool italic = false;
|
bool italic = false;
|
||||||
bool underline = false;
|
bool underline = false;
|
||||||
bool strikeOut = false;
|
bool strikeOut = false;
|
||||||
|
bool endingMarkers = false;
|
||||||
QString backticks(qtmw_Backtick);
|
QString backticks(qtmw_Backtick);
|
||||||
for (QTextBlock::Iterator frag = block.begin(); !frag.atEnd(); ++frag) {
|
for (QTextBlock::Iterator frag = block.begin(); !frag.atEnd(); ++frag) {
|
||||||
missedBlankCodeBlockLine = false;
|
missedBlankCodeBlockLine = false;
|
||||||
@ -549,19 +550,27 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
|
|||||||
if (startsOrEndsWithBacktick)
|
if (startsOrEndsWithBacktick)
|
||||||
markers += qtmw_Space;
|
markers += qtmw_Space;
|
||||||
mono = monoFrag;
|
mono = monoFrag;
|
||||||
|
if (!mono)
|
||||||
|
endingMarkers = true;
|
||||||
}
|
}
|
||||||
if (!blockFmt.headingLevel() && !mono) {
|
if (!blockFmt.headingLevel() && !mono) {
|
||||||
if (fontInfo.bold() != bold) {
|
if (fontInfo.bold() != bold) {
|
||||||
markers += "**"_L1;
|
markers += "**"_L1;
|
||||||
bold = fontInfo.bold();
|
bold = fontInfo.bold();
|
||||||
|
if (!bold)
|
||||||
|
endingMarkers = true;
|
||||||
}
|
}
|
||||||
if (fontInfo.italic() != italic) {
|
if (fontInfo.italic() != italic) {
|
||||||
markers += u'*';
|
markers += u'*';
|
||||||
italic = fontInfo.italic();
|
italic = fontInfo.italic();
|
||||||
|
if (!italic)
|
||||||
|
endingMarkers = true;
|
||||||
}
|
}
|
||||||
if (fontInfo.strikeOut() != strikeOut) {
|
if (fontInfo.strikeOut() != strikeOut) {
|
||||||
markers += "~~"_L1;
|
markers += "~~"_L1;
|
||||||
strikeOut = fontInfo.strikeOut();
|
strikeOut = fontInfo.strikeOut();
|
||||||
|
if (!strikeOut)
|
||||||
|
endingMarkers = true;
|
||||||
}
|
}
|
||||||
if (fontInfo.underline() != underline) {
|
if (fontInfo.underline() != underline) {
|
||||||
// Markdown doesn't support underline, but the parser will treat a single 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.
|
// That will have to do.
|
||||||
markers += u'_';
|
markers += u'_';
|
||||||
underline = fontInfo.underline();
|
underline = fontInfo.underline();
|
||||||
|
if (!underline)
|
||||||
|
endingMarkers = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -578,7 +589,8 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
|
|||||||
bool breakingLine = false;
|
bool breakingLine = false;
|
||||||
while (i < fragLen) {
|
while (i < fragLen) {
|
||||||
if (col >= ColumnLimit) {
|
if (col >= ColumnLimit) {
|
||||||
m_stream << qtmw_Newline << wrapIndentString;
|
m_stream << markers << qtmw_Newline << wrapIndentString;
|
||||||
|
markers.clear();
|
||||||
col = m_wrappedLineIndent;
|
col = m_wrappedLineIndent;
|
||||||
while (i < fragLen && fragmentText[i].isSpace())
|
while (i < fragLen && fragmentText[i].isSpace())
|
||||||
++i;
|
++i;
|
||||||
@ -588,6 +600,13 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
|
|||||||
int wi = nearestWordWrapIndex(fragmentText, j);
|
int wi = nearestWordWrapIndex(fragmentText, j);
|
||||||
if (wi < 0) {
|
if (wi < 0) {
|
||||||
j = fragLen;
|
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) {
|
} else if (wi >= i) {
|
||||||
j = wi;
|
j = wi;
|
||||||
breakingLine = true;
|
breakingLine = true;
|
||||||
@ -611,7 +630,7 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
|
|||||||
col += subfrag.size();
|
col += subfrag.size();
|
||||||
}
|
}
|
||||||
i = j + 1;
|
i = j + 1;
|
||||||
}
|
} // loop over fragment characters (we know we need to break somewhere)
|
||||||
} else {
|
} else {
|
||||||
if (!m_linePrefixWritten && col == wrapIndentString.size()) {
|
if (!m_linePrefixWritten && col == wrapIndentString.size()) {
|
||||||
m_stream << m_linePrefix;
|
m_stream << m_linePrefix;
|
||||||
|
@ -36,6 +36,8 @@ private slots:
|
|||||||
void testWriteNestedNumericLists();
|
void testWriteNestedNumericLists();
|
||||||
void testWriteNumericListWithStart();
|
void testWriteNumericListWithStart();
|
||||||
void testWriteTable();
|
void testWriteTable();
|
||||||
|
void charFormatWrapping_data();
|
||||||
|
void charFormatWrapping();
|
||||||
void rewriteDocument_data();
|
void rewriteDocument_data();
|
||||||
void rewriteDocument();
|
void rewriteDocument();
|
||||||
void fromHtml_data();
|
void fromHtml_data();
|
||||||
@ -525,6 +527,86 @@ void tst_QTextMarkdownWriter::testWriteTable()
|
|||||||
QCOMPARE(md, expected);
|
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()
|
void tst_QTextMarkdownWriter::rewriteDocument_data()
|
||||||
{
|
{
|
||||||
QTest::addColumn<QString>("inputFile");
|
QTest::addColumn<QString>("inputFile");
|
||||||
@ -569,7 +651,7 @@ void tst_QTextMarkdownWriter::fromHtml_data()
|
|||||||
|
|
||||||
QTest::newRow("long URL") <<
|
QTest::newRow("long URL") <<
|
||||||
"<span style=\"font-style:italic;\">https://www.example.com/dir/subdir/subsubdir/subsubsubdir/subsubsubsubdir/subsubsubsubsubdir/</span>" <<
|
"<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("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("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") <<
|
QTest::newRow("escaped asterisk after newline") <<
|
||||||
|
Loading…
x
Reference in New Issue
Block a user