Convert <pre> to Markdown ``` and vice-versa with nonBreakableLines

The HTML parser calls QTextBlockFormat::setNonBreakableLines(true) when
it sees a <pre> tag; so for symmetry, the markdown reader now does the
same when it sees a fenced code block, and the markdown writer honors
the nonBreakableLines property by writing a fenced code block. This
preserves the meaning better when reading HTML and writing markdown or
vice-versa, without modifying HTML reading or writing code.

Added a test tst_QTextMarkdownImporter::fencedCodeBlocks() which
unfortunately also highlights a known bug in the markdown reader: each
fenced code block ends with an extra empty block. That can be fixed
separately.

tst_QTextMarkdownWriter::fromHtml(preformats with embedded backticks)
that we re-enabled in 1abaf9d5d6ea9c6554362e851903ddd214a6f659 was not a
very useful test: ``` with a space and some words but no newline is not
a fence: it's just like a `monospace` span. We have had trouble with
those in CI because of missing monospace fonts, or inconsistency when
a supposedly mono font's QFontInfo::fixedPitch() returns false.
So just test proper <pre>/fence conversion for now.

Pick-to: 6.3
Fixes: QTBUG-100515
Fixes: QTBUG-100981
Task-number: QTBUG-101031
Change-Id: I88f0ede0810d8a9480b30eb0cd780e1af67cc5f2
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
This commit is contained in:
Shawn Rutledge 2022-02-18 21:58:35 +01:00
parent 53604df833
commit 04a60bb033
4 changed files with 82 additions and 4 deletions

View File

@ -571,8 +571,10 @@ void QTextMarkdownImporter::insertBlock()
}
if (m_codeBlock) {
blockFormat.setProperty(QTextFormat::BlockCodeLanguage, m_blockCodeLanguage);
if (m_blockCodeFence)
if (m_blockCodeFence) {
blockFormat.setNonBreakableLines(true);
blockFormat.setProperty(QTextFormat::BlockCodeFence, QString(QLatin1Char(m_blockCodeFence)));
}
charFormat.setFont(m_monoFont);
} else {
blockFormat.setTopMargin(m_paragraphMargin);

View File

@ -334,7 +334,8 @@ 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).length() > 0 ||
blockFmt.nonBreakableLines();
if (m_fencedCodeBlock && !codeBlock) {
m_stream << m_linePrefix << m_codeBlockFence << Newline;
m_fencedCodeBlock = false;

View File

@ -41,6 +41,8 @@ private slots:
void fragmentsAndProperties();
void pathological_data();
void pathological();
void fencedCodeBlocks_data();
void fencedCodeBlocks();
private:
bool isMainFontFixed();
@ -490,5 +492,78 @@ void tst_QTextMarkdownImporter::pathological() // avoid crashing on crazy input
QTextDocument().setMarkdown(f.readAll());
}
void tst_QTextMarkdownImporter::fencedCodeBlocks_data()
{
QTest::addColumn<QString>("input");
QTest::addColumn<int>("expectedCodeBlockCount");
QTest::addColumn<int>("expectedPlainBlockCount");
QTest::addColumn<QString>("expectedLanguage");
QTest::addColumn<QString>("expectedFenceChar");
QTest::addColumn<QString>("rewrite");
// TODO shouldn't add empty blocks: QTBUG-101031
QTest::newRow("backtick fence with language")
<< "```pseudocode\nprint('hello world\\n')\n```\n"
<< 2 << 0 << "pseudocode" << "`"
<< "```pseudocode\nprint('hello world\\n')\n```\n\n";
QTest::newRow("tilde fence with language")
<< "~~~pseudocode\nprint('hello world\\n')\n~~~\n"
<< 2 << 0 << "pseudocode" << "~"
<< "~~~pseudocode\nprint('hello world\\n')\n~~~\n\n";
QTest::newRow("embedded backticks")
<< "```\nnone `one` ``two``\n```\nplain\n```\n```three``` ````four````\n```\nplain\n"
<< 4 << 2 << QString() << "`"
<< "```\nnone `one` ``two``\n```\nplain\n\n```\n```three``` ````four````\n```\nplain\n\n";
}
void tst_QTextMarkdownImporter::fencedCodeBlocks()
{
QFETCH(QString, input);
QFETCH(int, expectedCodeBlockCount);
QFETCH(int, expectedPlainBlockCount);
QFETCH(QString, expectedLanguage);
QFETCH(QString, expectedFenceChar);
QFETCH(QString, rewrite);
QTextDocument doc;
doc.setMarkdown(input);
#ifdef DEBUG_WRITE_HTML
{
QFile out("/tmp/" + QLatin1String(QTest::currentDataTag()) + ".html");
out.open(QFile::WriteOnly);
out.write(doc.toHtml().toLatin1());
out.close();
}
#endif
QTextFrame::iterator iterator = doc.rootFrame()->begin();
QTextFrame *currentFrame = iterator.currentFrame();
int codeBlockCount = 0;
int plainBlockCount = 0;
while (!iterator.atEnd()) {
// There are no child frames
QCOMPARE(iterator.currentFrame(), currentFrame);
// Check whether the block is code or plain
QTextBlock block = iterator.currentBlock();
const bool codeBlock = block.blockFormat().hasProperty(QTextFormat::BlockCodeFence);
QCOMPARE(block.blockFormat().nonBreakableLines(), codeBlock);
QCOMPARE(block.blockFormat().stringProperty(QTextFormat::BlockCodeLanguage), codeBlock ? expectedLanguage : QString());
if (codeBlock) {
QCOMPARE(block.blockFormat().stringProperty(QTextFormat::BlockCodeFence), expectedFenceChar);
++codeBlockCount;
} else {
++plainBlockCount;
}
qCDebug(lcTests) << (codeBlock ? "code" : "text") << block.text() << block.charFormat().fontFamilies();
++iterator;
}
QCOMPARE(codeBlockCount, expectedCodeBlockCount);
QCOMPARE(plainBlockCount, expectedPlainBlockCount);
if (doc.toMarkdown() != rewrite && isMainFontFixed())
QEXPECT_FAIL("", "fixed-pitch main font (QTBUG-103484)", Continue);
QCOMPARE(doc.toMarkdown(), rewrite);
}
QTEST_MAIN(tst_QTextMarkdownImporter)
#include "tst_qtextmarkdownimporter.moc"

View File

@ -500,8 +500,8 @@ void tst_QTextMarkdownWriter::fromHtml_data()
// "<p>(The first sentence of this paragraph is a line, the next paragraph has a number</p>13) but that's not part of an ordered list" <<
// "(The first sentence of this paragraph is a line, the next paragraph has a number\n\n13\\) but that's not part of an ordered list\n\n";
QTest::newRow("preformats with embedded backticks") <<
"<pre>none `one` ``two``</pre><pre>```three``` ````four````</pre>plain" <<
"``` none `one` ``two`` ```\n\n````` ```three``` ````four```` `````\n\nplain\n\n";
"<pre>none `one` ``two``</pre>plain<pre>```three``` ````four````</pre>plain" <<
"```\nnone `one` ``two``\n\n```\nplain\n\n```\n```three``` ````four````\n\n```\nplain\n\n";
}
void tst_QTextMarkdownWriter::fromHtml()