From 04a60bb033f57099b048b0d6f99f68a08dcd1483 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Fri, 18 Feb 2022 21:58:35 +0100 Subject: [PATCH] Convert
 to Markdown ``` and vice-versa with
 nonBreakableLines

The HTML parser calls QTextBlockFormat::setNonBreakableLines(true) when
it sees a 
 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 
/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 
---
 src/gui/text/qtextmarkdownimporter.cpp        |  4 +-
 src/gui/text/qtextmarkdownwriter.cpp          |  3 +-
 .../tst_qtextmarkdownimporter.cpp             | 75 +++++++++++++++++++
 .../tst_qtextmarkdownwriter.cpp               |  4 +-
 4 files changed, 82 insertions(+), 4 deletions(-)

diff --git a/src/gui/text/qtextmarkdownimporter.cpp b/src/gui/text/qtextmarkdownimporter.cpp
index 37947373c16..c434bf859aa 100644
--- a/src/gui/text/qtextmarkdownimporter.cpp
+++ b/src/gui/text/qtextmarkdownimporter.cpp
@@ -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);
diff --git a/src/gui/text/qtextmarkdownwriter.cpp b/src/gui/text/qtextmarkdownwriter.cpp
index 46a8318963d..fe772d3944e 100644
--- a/src/gui/text/qtextmarkdownwriter.cpp
+++ b/src/gui/text/qtextmarkdownwriter.cpp
@@ -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;
diff --git a/tests/auto/gui/text/qtextmarkdownimporter/tst_qtextmarkdownimporter.cpp b/tests/auto/gui/text/qtextmarkdownimporter/tst_qtextmarkdownimporter.cpp
index 87a8f4aa898..b8d53ca194e 100644
--- a/tests/auto/gui/text/qtextmarkdownimporter/tst_qtextmarkdownimporter.cpp
+++ b/tests/auto/gui/text/qtextmarkdownimporter/tst_qtextmarkdownimporter.cpp
@@ -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("input");
+    QTest::addColumn("expectedCodeBlockCount");
+    QTest::addColumn("expectedPlainBlockCount");
+    QTest::addColumn("expectedLanguage");
+    QTest::addColumn("expectedFenceChar");
+    QTest::addColumn("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"
diff --git a/tests/auto/gui/text/qtextmarkdownwriter/tst_qtextmarkdownwriter.cpp b/tests/auto/gui/text/qtextmarkdownwriter/tst_qtextmarkdownwriter.cpp
index b9230be465e..c5e48295330 100644
--- a/tests/auto/gui/text/qtextmarkdownwriter/tst_qtextmarkdownwriter.cpp
+++ b/tests/auto/gui/text/qtextmarkdownwriter/tst_qtextmarkdownwriter.cpp
@@ -500,8 +500,8 @@ void tst_QTextMarkdownWriter::fromHtml_data()
 //        "

(The first sentence of this paragraph is a line, the next paragraph has a number

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") << - "
none `one` ``two``
```three``` ````four````
plain" << - "``` none `one` ``two`` ```\n\n````` ```three``` ````four```` `````\n\nplain\n\n"; + "
none `one` ``two``
plain
```three``` ````four````
plain" << + "```\nnone `one` ``two``\n\n```\nplain\n\n```\n```three``` ````four````\n\n```\nplain\n\n"; } void tst_QTextMarkdownWriter::fromHtml()