QTextMarkdownWriter: Handle lists in blockquotes correctly

But we do not yet handle a blockquote in a list item. Presumably
that's less common anyway.

We now also continue block-quote prefixes onto blank lines
within a block quote, which looks more normal.

Task-number: QTBUG-104997
Change-Id: I2b5642cf3a0c81a94444a33f026a02ad53e7e6bb
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Axel Spoerl <axel.spoerl@qt.io>
(cherry picked from commit f3e528b97f6836b497953935e1dd27fee134e68a)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
Shawn Rutledge 2022-07-15 11:31:51 +02:00 committed by Qt Cherry-pick Bot
parent a1413a6ecd
commit 3b4901d2d4
6 changed files with 80 additions and 20 deletions

View File

@ -112,17 +112,22 @@ void QTextMarkdownWriter::writeFrame(const QTextFrame *frame)
// suppress needless blank lines, when there will be a big change in block format // suppress needless blank lines, when there will be a big change in block format
bool nextIsDifferent = false; bool nextIsDifferent = false;
bool ending = false; bool ending = false;
int blockQuoteIndent = 0;
int nextBlockQuoteIndent = 0;
{ {
QTextFrame::iterator next = iterator; QTextFrame::iterator next = iterator;
++next; ++next;
QTextBlockFormat format = iterator.currentBlock().blockFormat();
QTextBlockFormat nextFormat = next.currentBlock().blockFormat();
blockQuoteIndent = format.intProperty(QTextFormat::BlockQuoteLevel);
nextBlockQuoteIndent = nextFormat.intProperty(QTextFormat::BlockQuoteLevel);
if (next.atEnd()) { if (next.atEnd()) {
nextIsDifferent = true; nextIsDifferent = true;
ending = true; ending = true;
} else { } else {
QTextBlockFormat format = iterator.currentBlock().blockFormat();
QTextBlockFormat nextFormat = next.currentBlock().blockFormat();
if (nextFormat.indent() != format.indent() || if (nextFormat.indent() != format.indent() ||
nextFormat.property(QTextFormat::BlockCodeLanguage) != format.property(QTextFormat::BlockCodeLanguage)) nextFormat.property(QTextFormat::BlockCodeLanguage) !=
format.property(QTextFormat::BlockCodeLanguage))
nextIsDifferent = true; nextIsDifferent = true;
} }
} }
@ -139,8 +144,10 @@ void QTextMarkdownWriter::writeFrame(const QTextFrame *frame)
tableRow = cell.row(); tableRow = cell.row();
} }
} else if (!block.textList()) { } else if (!block.textList()) {
if (lastWasList) if (lastWasList) {
m_stream << qtmw_Newline; m_stream << qtmw_Newline;
m_linePrefixWritten = false;
}
} }
int endingCol = writeBlock(block, !table, table && tableRow == 0, int endingCol = writeBlock(block, !table, table && tableRow == 0,
nextIsDifferent && !block.textList()); nextIsDifferent && !block.textList());
@ -164,8 +171,16 @@ void QTextMarkdownWriter::writeFrame(const QTextFrame *frame)
} else if (endingCol > 0) { } else if (endingCol > 0) {
if (block.textList() || block.blockFormat().hasProperty(QTextFormat::BlockCodeLanguage)) { if (block.textList() || block.blockFormat().hasProperty(QTextFormat::BlockCodeLanguage)) {
m_stream << qtmw_Newline; m_stream << qtmw_Newline;
if (block.textList()) {
m_stream << m_linePrefix;
m_linePrefixWritten = true;
}
} else { } else {
m_stream << qtmw_Newline << qtmw_Newline; m_stream << qtmw_Newline;
if (nextBlockQuoteIndent < blockQuoteIndent)
setLinePrefixForBlockQuote(nextBlockQuoteIndent);
m_stream << m_linePrefix;
m_stream << qtmw_Newline;
m_doubleNewlineWritten = true; m_doubleNewlineWritten = true;
} }
} }
@ -211,6 +226,16 @@ QTextMarkdownWriter::ListInfo QTextMarkdownWriter::listInfo(QTextList *list)
return m_listInfo.value(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) static int nearestWordWrapIndex(const QString &s, int before)
{ {
before = qMin(before, s.size()); before = qMin(before, s.size());
@ -336,10 +361,20 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
const bool codeBlock = blockFmt.hasProperty(QTextFormat::BlockCodeFence) || const bool codeBlock = blockFmt.hasProperty(QTextFormat::BlockCodeFence) ||
blockFmt.stringProperty(QTextFormat::BlockCodeLanguage).size() > 0 || blockFmt.stringProperty(QTextFormat::BlockCodeLanguage).size() > 0 ||
blockFmt.nonBreakableLines(); blockFmt.nonBreakableLines();
const int blockQuoteLevel = blockFmt.intProperty(QTextFormat::BlockQuoteLevel);
if (m_fencedCodeBlock && !codeBlock) { if (m_fencedCodeBlock && !codeBlock) {
m_stream << m_linePrefix << m_codeBlockFence << qtmw_Newline; m_stream << m_linePrefix << m_codeBlockFence << qtmw_Newline;
m_fencedCodeBlock = false; m_fencedCodeBlock = false;
m_codeBlockFence.clear(); 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 if (block.textList()) { // it's a list-item
auto fmt = block.textList()->format(); auto fmt = block.textList()->format();
@ -416,31 +451,25 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
if (blockFmt.hasProperty(QTextFormat::BlockIndent)) if (blockFmt.hasProperty(QTextFormat::BlockIndent))
m_codeBlockFence = QString(m_wrappedLineIndent, qtmw_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. // A block quote can contain an indented code block, but not vice-versa.
m_stream << m_linePrefix << m_codeBlockFence m_stream << m_codeBlockFence << blockFmt.stringProperty(QTextFormat::BlockCodeLanguage)
<< blockFmt.stringProperty(QTextFormat::BlockCodeLanguage) << qtmw_Newline; << qtmw_Newline << m_linePrefix;
m_fencedCodeBlock = true; m_fencedCodeBlock = true;
} }
wrap = false; wrap = false;
} else if (!blockFmt.indent()) { } else if (!blockFmt.indent()) {
m_wrappedLineIndent = 0; 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)) { if (blockFmt.hasProperty(QTextFormat::BlockCodeLanguage)) {
// A block quote can contain an indented code block, but not vice-versa. // A block quote can contain an indented code block, but not vice-versa.
m_linePrefix += QString(4, qtmw_Space); m_linePrefix += QString(4, qtmw_Space);
m_indentedCodeBlock = true; m_indentedCodeBlock = true;
} }
if (!m_linePrefixWritten) {
m_stream << m_linePrefix;
m_linePrefixWritten = true;
}
} }
if (blockFmt.headingLevel()) if (blockFmt.headingLevel())
m_stream << QByteArray(blockFmt.headingLevel(), '#') << ' '; m_stream << QByteArray(blockFmt.headingLevel(), '#') << ' ';
else
m_stream << m_linePrefix;
QString wrapIndentString = m_linePrefix + QString(m_wrappedLineIndent, qtmw_Space); QString wrapIndentString = m_linePrefix + QString(m_wrappedLineIndent, qtmw_Space);
// It would be convenient if QTextStream had a lineCharPos() accessor, // It would be convenient if QTextStream had a lineCharPos() accessor,
@ -584,6 +613,10 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
i = j + 1; i = j + 1;
} }
} else { } else {
if (!m_linePrefixWritten && col == wrapIndentString.size()) {
m_stream << m_linePrefix;
col += m_linePrefix.size();
}
m_stream << markers << fragmentText; m_stream << markers << fragmentText;
col += markers.size() + fragmentText.size(); col += markers.size() + fragmentText.size();
} }
@ -615,6 +648,7 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
} }
if (missedBlankCodeBlockLine) if (missedBlankCodeBlockLine)
m_stream << qtmw_Newline; m_stream << qtmw_Newline;
m_linePrefixWritten = false;
return col; return col;
} }

View File

@ -43,6 +43,7 @@ private:
}; };
ListInfo listInfo(QTextList *list); ListInfo listInfo(QTextList *list);
void setLinePrefixForBlockQuote(int level);
private: private:
QTextStream &m_stream; QTextStream &m_stream;
@ -53,6 +54,7 @@ private:
int m_wrappedLineIndent = 0; int m_wrappedLineIndent = 0;
int m_lastListIndent = 1; int m_lastListIndent = 1;
bool m_doubleNewlineWritten = false; bool m_doubleNewlineWritten = false;
bool m_linePrefixWritten = false;
bool m_indentedCodeBlock = false; bool m_indentedCodeBlock = false;
bool m_fencedCodeBlock = false; bool m_fencedCodeBlock = false;
}; };

View File

@ -8,17 +8,17 @@ MacFarlane writes:
> What distinguishes Markdown from many other lightweight markup syntaxes, > What distinguishes Markdown from many other lightweight markup syntaxes,
> which are often easier to write, is its readability. As Gruber writes: > which are often easier to write, is its readability. As Gruber writes:
>
> > The overriding design goal for Markdown's formatting syntax is to make it > > The overriding design goal for Markdown's formatting syntax is to make it
> > as readable as possible. The idea is that a Markdown-formatted document should > > as readable as possible. The idea is that a Markdown-formatted document should
> > be publishable as-is, as plain text, without looking like it's been marked up > > be publishable as-is, as plain text, without looking like it's been marked up
> > with tags or formatting instructions. ( > > with tags or formatting instructions. (
> > <http://daringfireball.net/projects/markdown/> ) > > <http://daringfireball.net/projects/markdown/> )
>
> The point can be illustrated by comparing a sample of AsciiDoc with an > The point can be illustrated by comparing a sample of AsciiDoc with an
> equivalent sample of Markdown. Here is a sample of AsciiDoc from the AsciiDoc > equivalent sample of Markdown. Here is a sample of AsciiDoc from the AsciiDoc
> manual: > manual:
>
> ```AsciiDoc > ```AsciiDoc
> 1. List item one. > 1. List item one.
> + > +

View File

@ -0,0 +1,14 @@
What if we have a quotation containing a list?
> First some quoted text, and then a list:
>
> - one
> - two is longer and has enough words to form a paragraph with text continuing
> onto the next line
>
> enough of that, let's try a numbered list
>
> 1. List item one
> 2. List item two is longer and has enough words to form a paragraph with
> text continuing onto the next line.
>

View File

@ -0,0 +1,6 @@
What if we have a list item containing a block quote?
- one
- > two is longer and has enough words to form a paragraph with text continuing
> onto the next line

View File

@ -530,6 +530,8 @@ void tst_QTextMarkdownWriter::rewriteDocument_data()
QTest::addColumn<QString>("inputFile"); QTest::addColumn<QString>("inputFile");
QTest::newRow("block quotes") << "blockquotes.md"; QTest::newRow("block quotes") << "blockquotes.md";
QTest::newRow("block quotes with lists") << "blockquotesWithLists.md";
// QTest::newRow("list item with block quote") << "listItemWithBlockquote.md"; // not supported for now
QTest::newRow("example") << "example.md"; QTest::newRow("example") << "example.md";
QTest::newRow("list items after headings") << "headingsAndLists.md"; QTest::newRow("list items after headings") << "headingsAndLists.md";
QTest::newRow("word wrap") << "wordWrap.md"; QTest::newRow("word wrap") << "wordWrap.md";
@ -629,6 +631,8 @@ void tst_QTextMarkdownWriter::fromHtml()
} }
#endif #endif
output = output.trimmed();
expectedOutput = expectedOutput.trimmed();
if (output != expectedOutput && (isMainFontFixed() || isFixedFontProportional())) if (output != expectedOutput && (isMainFontFixed() || isFixedFontProportional()))
QEXPECT_FAIL("", "fixed main font or proportional fixed font (QTBUG-103484)", Continue); QEXPECT_FAIL("", "fixed main font or proportional fixed font (QTBUG-103484)", Continue);
QCOMPARE(output, expectedOutput); QCOMPARE(output, expectedOutput);