Add QTextDocFragment::to/fromMarkdown() & QTextCursor::insertMarkdown()

Also add the beginnings of an autotest for QTextCursor::insertHtml(),
for comparison purposes.

We can see that the block to be inserted is merged with an existing
block by default rather than being inserted as a new one, with both HTML and
Markdown insertions.  So now we test for leading and trailing newlines
in the markdown to be inserted, to determine whether we need a new block
into which to insert, and to "hit enter" at the end of the insertion.

QSKIP the toMarkdown() comparisons if GeneralFont is mono. This happens
on Boot2Qt systems in CI.

Task-number: QTBUG-76105
Task-number: QTBUG-94462
Task-number: QTBUG-100515
Task-number: QTBUG-103484
Change-Id: I51a05c6a7cd0be4f2817f4a922f45fa663982293
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
This commit is contained in:
Shawn Rutledge 2021-06-15 16:50:44 +02:00
parent e69ebf93ca
commit 7c76064604
5 changed files with 310 additions and 6 deletions

View File

@ -2290,6 +2290,28 @@ void QTextCursor::insertHtml(const QString &html)
#endif // QT_NO_TEXTHTMLPARSER
/*!
\since 6.4
Inserts the \a markdown text at the current position(),
with the specified Markdown \a features. The default is GitHub dialect.
*/
#if QT_CONFIG(textmarkdownreader)
void QTextCursor::insertMarkdown(const QString &markdown, QTextDocument::MarkdownFeatures features)
{
if (!d || !d->priv)
return;
QTextDocumentFragment fragment = QTextDocumentFragment::fromMarkdown(markdown, features);
if (markdown.startsWith(QLatin1Char('\n')))
insertBlock(fragment.d->doc->firstBlock().blockFormat());
insertFragment(fragment);
if (!atEnd() && markdown.endsWith(QLatin1Char('\n')))
insertText(QLatin1String("\n"));
}
#endif // textmarkdownreader
/*!
\overload
\since 4.2

View File

@ -43,12 +43,11 @@
#include <QtGui/qtguiglobal.h>
#include <QtCore/qstring.h>
#include <QtCore/qshareddata.h>
#include <QtGui/qtextdocument.h>
#include <QtGui/qtextformat.h>
QT_BEGIN_NAMESPACE
class QTextDocument;
class QTextCursorPrivate;
class QTextDocumentFragment;
class QTextCharFormat;
@ -201,6 +200,10 @@ public:
#ifndef QT_NO_TEXTHTMLPARSER
void insertHtml(const QString &html);
#endif // QT_NO_TEXTHTMLPARSER
#if QT_CONFIG(textmarkdownreader)
void insertMarkdown(const QString &markdown,
QTextDocument::MarkdownFeatures features = QTextDocument::MarkdownDialectGitHub);
#endif // textmarkdownreader
void insertImage(const QTextImageFormat &format, QTextFrameFormat::Position alignment);
void insertImage(const QTextImageFormat &format);

View File

@ -41,6 +41,12 @@
#include "qtextdocumentfragment_p.h"
#include "qtextcursor_p.h"
#include "qtextlist.h"
#if QT_CONFIG(textmarkdownreader)
#include "qtextmarkdownimporter_p.h"
#endif
#if QT_CONFIG(textmarkdownwriter)
#include "qtextmarkdownwriter_p.h"
#endif
#include <qdebug.h>
#include <qbytearray.h>
@ -412,6 +418,26 @@ QString QTextDocumentFragment::toHtml() const
#endif // QT_NO_TEXTHTMLPARSER
#if QT_CONFIG(textmarkdownwriter)
/*!
\since 6.4
Returns the contents of the document fragment as Markdown,
with the specified \a features. The default is GitHub dialect.
\sa toPlainText(), QTextDocument::toMarkdown()
*/
QString QTextDocumentFragment::toMarkdown(QTextDocument::MarkdownFeatures features) const
{
if (!d)
return QString();
return d->doc->toMarkdown(features);
}
#endif // textmarkdownwriter
/*!
Returns a document fragment that contains the given \a plainText.
@ -1277,9 +1303,6 @@ void QTextHtmlImporter::appendBlock(const QTextBlockFormat &format, QTextCharFor
compressNextWhitespace = RemoveWhiteSpace;
}
#endif // QT_NO_TEXTHTMLPARSER
#ifndef QT_NO_TEXTHTMLPARSER
/*!
\fn QTextDocumentFragment QTextDocumentFragment::fromHtml(const QString &text, const QTextDocument *resourceProvider)
\since 4.2
@ -1305,4 +1328,31 @@ QTextDocumentFragment QTextDocumentFragment::fromHtml(const QString &html, const
#endif // QT_NO_TEXTHTMLPARSER
#if QT_CONFIG(textmarkdownreader)
/*!
\fn QTextDocumentFragment QTextDocumentFragment::fromMarkdown(const QString &markdown, QTextDocument::MarkdownFeatures features)
\since 6.4
Returns a QTextDocumentFragment based on the given \a markdown text with
the specified \a features. The default is GitHub dialect.
The formatting is preserved as much as possible; for example, \c {**bold**}
will become a document fragment containing the text "bold" with a bold
character style.
\note Loading external resources is not supported.
*/
QTextDocumentFragment QTextDocumentFragment::fromMarkdown(const QString &markdown, QTextDocument::MarkdownFeatures features)
{
QTextDocumentFragment res;
res.d = new QTextDocumentFragmentPrivate;
QTextMarkdownImporter importer(features);
importer.import(res.d->doc, markdown);
return res;
}
#endif // textmarkdownreader
QT_END_NAMESPACE

View File

@ -41,13 +41,13 @@
#define QTEXTDOCUMENTFRAGMENT_H
#include <QtGui/qtguiglobal.h>
#include <QtGui/qtextdocument.h>
#include <QtCore/qstring.h>
QT_BEGIN_NAMESPACE
class QTextStream;
class QTextDocument;
class QTextDocumentFragmentPrivate;
class QTextCursor;
@ -68,11 +68,18 @@ public:
#ifndef QT_NO_TEXTHTMLPARSER
QString toHtml() const;
#endif // QT_NO_TEXTHTMLPARSER
#if QT_CONFIG(textmarkdownwriter)
QString toMarkdown(QTextDocument::MarkdownFeatures features = QTextDocument::MarkdownDialectGitHub) const;
#endif
static QTextDocumentFragment fromPlainText(const QString &plainText);
#ifndef QT_NO_TEXTHTMLPARSER
static QTextDocumentFragment fromHtml(const QString &html, const QTextDocument *resourceProvider = nullptr);
#endif // QT_NO_TEXTHTMLPARSER
#if QT_CONFIG(textmarkdownreader)
static QTextDocumentFragment fromMarkdown(const QString &markdown,
QTextDocument::MarkdownFeatures features = QTextDocument::MarkdownDialectGitHub);
#endif
private:
QTextDocumentFragmentPrivate *d;

View File

@ -28,7 +28,9 @@
#include <QTest>
#include <QLoggingCategory>
#include <qfontinfo.h>
#include <qtextdocument.h>
#include <qtexttable.h>
#include <qvariant.h>
@ -41,6 +43,8 @@
#include <private/qtextcursor_p.h>
Q_LOGGING_CATEGORY(lcTests, "qt.gui.tests")
QT_FORWARD_DECLARE_CLASS(QTextDocument)
class tst_QTextCursor : public QObject
@ -110,6 +114,14 @@ private slots:
void selectVisually();
void insertText();
#ifndef QT_NO_TEXTHTMLPARSER
void insertHtml_data();
void insertHtml();
#endif
#if QT_CONFIG(textmarkdownreader)
void insertMarkdown_data();
void insertMarkdown();
#endif
void insertFragmentShouldUseCurrentCharFormat();
@ -1428,6 +1440,216 @@ void tst_QTextCursor::insertText()
QCOMPARE(cursor.block().text(), QString("yoyodyne"));
}
#ifndef QT_NO_TEXTHTMLPARSER
void tst_QTextCursor::insertHtml_data()
{
QTest::addColumn<QString>("initialText");
QTest::addColumn<int>("expectedInitialBlockCount");
QTest::addColumn<bool>("insertBlock");
QTest::addColumn<bool>("insertAsPlainText");
QTest::addColumn<int>("insertPosition");
QTest::addColumn<QString>("insertText");
QTest::addColumn<QString>("expectedSelText");
QTest::addColumn<QString>("expectedText");
QTest::addColumn<QString>("expectedMarkdown");
const QString htmlHeadingString("<h1>Hello World</h1>");
QTest::newRow("insert as html at end of heading")
<< htmlHeadingString << 1
<< false << false << 11 << QString("Other\ntext")
<< QString("Hello WorldOther text")
<< QString("Hello WorldOther text")
<< QString("# Hello WorldOther text\n\n");
QTest::newRow("insert as html in new block at end of heading")
<< htmlHeadingString << 1
<< false << true << 11 << QString("Other\ntext")
<< QString("Hello WorldOther\u2029text")
<< QString("Hello WorldOther\ntext")
<< QString("# Hello WorldOther\n\n# text\n\n");
QTest::newRow("insert as html in middle of heading")
<< htmlHeadingString << 1
<< false << false << 6 << QString("\n\nOther\ntext\n\n")
<< QString("Hello Other text World")
<< QString("Hello Other text World")
<< QString("# Hello Other text World\n\n");
QTest::newRow("insert as text at end of heading")
<< htmlHeadingString << 1
<< true << false << 11 << QString("\n\nOther\ntext")
<< QString("Hello World\u2029Other text")
<< QString("Hello World\nOther text")
<< QString("# Hello World\n\nOther text\n\n");
QTest::newRow("insert as text in new block at end of heading")
<< htmlHeadingString << 1
<< true << true << 11 << QString("\n\nOther\ntext")
<< QString("Hello World\u2029\u2029\u2029Other\u2029text")
<< QString("Hello World\n\n\nOther\ntext")
<< QString("# Hello World\n\n**Other**\n\n**text**\n\n");
QTest::newRow("insert as text in middle of heading")
<< htmlHeadingString << 1
<< true << false << 6 << QString("Other\ntext")
<< QString("Hello \u2029Other textWorld")
<< QString("Hello \nOther textWorld")
<< QString("# Hello \n\nOther text**World**\n\n");
}
void tst_QTextCursor::insertHtml()
{
QFETCH(QString, initialText);
QFETCH(int, expectedInitialBlockCount);
QFETCH(bool, insertBlock);
QFETCH(bool, insertAsPlainText);
QFETCH(int, insertPosition);
QFETCH(QString, insertText);
QFETCH(QString, expectedSelText);
QFETCH(QString, expectedText);
QFETCH(QString, expectedMarkdown);
cursor.insertHtml(initialText);
QCOMPARE(blockCount(), expectedInitialBlockCount);
cursor.setPosition(insertPosition);
if (insertBlock)
cursor.insertBlock(QTextBlockFormat());
qCDebug(lcTests) << "pos" << cursor.position() << "block" << cursor.blockNumber()
<< "heading" << cursor.blockFormat().headingLevel();
if (insertAsPlainText)
cursor.insertText(insertText);
else
cursor.insertHtml(insertText);
cursor.select(QTextCursor::Document);
qCDebug(lcTests) << "sel text after insertion" << cursor.selectedText();
qCDebug(lcTests) << "text after insertion" << cursor.document()->toPlainText();
qCDebug(lcTests) << "html after insertion" << cursor.document()->toHtml();
qCDebug(lcTests) << "markdown after insertion" << cursor.document()->toMarkdown();
QCOMPARE(cursor.selectedText(), expectedSelText);
QCOMPARE(cursor.document()->toPlainText(), expectedText);
if (auto defaultFont = QFontDatabase::systemFont(QFontDatabase::GeneralFont); QFontInfo(defaultFont).fixedPitch()) {
qWarning() << defaultFont << "is QFontDatabase::GeneralFont, and is fixedPitch";
QSKIP("cannot reliably distinguish normal and monospace markdown spans on this system (QTBUG-103484)");
}
QCOMPARE(cursor.document()->toMarkdown(), expectedMarkdown);
}
#endif // QT_NO_TEXTHTMLPARSER
#if QT_CONFIG(textmarkdownreader)
void tst_QTextCursor::insertMarkdown_data()
{
QTest::addColumn<QString>("initialText");
QTest::addColumn<int>("expectedInitialBlockCount");
QTest::addColumn<int>("insertPosition");
QTest::addColumn<QString>("insertText");
QTest::addColumn<QString>("expectedSelText");
QTest::addColumn<QString>("expectedText");
QTest::addColumn<QString>("expectedMarkdown");
QTest::newRow("bold fragment in italic span")
<< "someone said *hello world*" << 1
<< 19 << QString(" **crazy** ")
<< QString("someone said hello crazyworld")
<< QString("someone said hello crazyworld")
<< QString("someone said *hello ***crazy***world*\n\n"); // explicit B+I: not necessary but OK
QTest::newRow("list in a paragraph")
<< "hello list with 3 items" << 1
<< 10 << QString("1. one\n2. two\n")
<< QString("hello list\u2029one\u2029two\u2029 with 3 items")
<< QString("hello list\none\ntwo\n with 3 items")
<< QString("hello list\n\n1. one\n2. two\n3. with 3 items\n");
QTest::newRow("list in a list")
<< "1) bread\n2) milk\n" << 2
<< 6 << QString("0) eggs\n1) maple syrup\n")
<< QString("bread\u2029eggs\u2029maple syrup\u2029milk")
<< QString("bread\neggs\nmaple syrup\nmilk")
<< QString("1) bread\n2) eggs\n1) maple syrup\n2) milk\n");
// renumbering would happen if we re-read the whole document
QTest::newRow("list after a list")
<< "1) bread\n2) milk\n\n" << 2
<< 13 << QString("\n0) eggs\n1) maple syrup\n")
<< QString("bread\u2029milk\u2029eggs\u2029maple syrup")
<< QString("bread\nmilk\neggs\nmaple syrup")
<< QString("1) bread\n2) milk\n3) eggs\n1) maple syrup\n");
const QString markdownHeadingString("# Hello\nWorld\n");
QTest::newRow("markdown heading at end of markdown heading")
<< markdownHeadingString << 2
<< 11 << QString("\n\n## Other text")
<< QString("Hello\u2029World\u2029Other text")
<< QString("Hello\nWorld\nOther text")
<< QString("# Hello\n\nWorld\n\n## Other text\n\n");
QTest::newRow("markdown heading into middle of markdown heading")
<< markdownHeadingString << 2
<< 6 << QString("## Other\ntext\n\n")
<< QString("Hello\u2029Other\u2029text\u2029World")
<< QString("Hello\nOther\ntext\nWorld")
<< QString("# Hello\n\n**Other**\n\ntext\n\nWorld\n\n");
QTest::newRow("markdown heading without trailing newline into middle of markdown heading")
<< markdownHeadingString << 2
<< 6 << QString("## Other\ntext")
<< QString("Hello\u2029Other\u2029textWorld")
<< QString("Hello\nOther\ntextWorld")
<< QString("# Hello\n\n**Other**\n\ntextWorld\n\n");
QTest::newRow("text into middle of markdown heading after newline")
<< markdownHeadingString << 2
<< 6 << QString("Other ")
<< QString("Hello\u2029OtherWorld")
<< QString("Hello\nOtherWorld")
<< QString("# Hello\n\nOtherWorld\n\n");
QTest::newRow("text into middle of markdown heading before newline")
<< markdownHeadingString << 2
<< 5 << QString(" Other ")
<< QString("HelloOther\u2029World")
<< QString("HelloOther\nWorld")
<< QString("# HelloOther\n\nWorld\n\n");
}
void tst_QTextCursor::insertMarkdown()
{
QFETCH(QString, initialText);
QFETCH(int, expectedInitialBlockCount);
QFETCH(int, insertPosition);
QFETCH(QString, insertText);
QFETCH(QString, expectedSelText);
QFETCH(QString, expectedText);
QFETCH(QString, expectedMarkdown);
cursor.insertMarkdown(initialText);
QCOMPARE(blockCount(), expectedInitialBlockCount);
cursor.setPosition(insertPosition);
qCDebug(lcTests) << "pos" << cursor.position() << "block" << cursor.blockNumber()
<< "heading" << cursor.blockFormat().headingLevel();
cursor.insertMarkdown(insertText);
cursor.select(QTextCursor::Document);
qCDebug(lcTests) << "sel text after insertion" << cursor.selectedText();
qCDebug(lcTests) << "text after insertion" << cursor.document()->toPlainText();
qCDebug(lcTests) << "html after insertion" << cursor.document()->toHtml();
qCDebug(lcTests) << "markdown after insertion" << cursor.document()->toMarkdown();
QCOMPARE(cursor.selectedText(), expectedSelText);
QCOMPARE(cursor.document()->toPlainText(), expectedText);
if (auto defaultFont = QFontDatabase::systemFont(QFontDatabase::GeneralFont); QFontInfo(defaultFont).fixedPitch()) {
qWarning() << defaultFont << "is QFontDatabase::GeneralFont, and is fixedPitch";
QSKIP("cannot reliably distinguish normal and monospace markdown spans on this system (QTBUG-103484)");
}
QCOMPARE(cursor.document()->toMarkdown(), expectedMarkdown);
}
#endif // textmarkdownreader
void tst_QTextCursor::insertFragmentShouldUseCurrentCharFormat()
{
QTextDocumentFragment fragment = QTextDocumentFragment::fromPlainText("Hello World");