diff options
-rw-r--r-- | src/gui/text/qtextcursor.cpp | 22 | ||||
-rw-r--r-- | src/gui/text/qtextcursor.h | 7 | ||||
-rw-r--r-- | src/gui/text/qtextdocumentfragment.cpp | 56 | ||||
-rw-r--r-- | src/gui/text/qtextdocumentfragment.h | 9 | ||||
-rw-r--r-- | tests/auto/gui/text/qtextcursor/tst_qtextcursor.cpp | 222 |
5 files changed, 310 insertions, 6 deletions
diff --git a/src/gui/text/qtextcursor.cpp b/src/gui/text/qtextcursor.cpp index 8a8b2efaef..1480020410 100644 --- a/src/gui/text/qtextcursor.cpp +++ b/src/gui/text/qtextcursor.cpp @@ -2291,6 +2291,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 diff --git a/src/gui/text/qtextcursor.h b/src/gui/text/qtextcursor.h index b33b05aacc..890712ab22 100644 --- a/src/gui/text/qtextcursor.h +++ b/src/gui/text/qtextcursor.h @@ -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); diff --git a/src/gui/text/qtextdocumentfragment.cpp b/src/gui/text/qtextdocumentfragment.cpp index 348916dd04..47baa4229e 100644 --- a/src/gui/text/qtextdocumentfragment.cpp +++ b/src/gui/text/qtextdocumentfragment.cpp @@ -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 diff --git a/src/gui/text/qtextdocumentfragment.h b/src/gui/text/qtextdocumentfragment.h index 37d7006ae6..3c23e403e0 100644 --- a/src/gui/text/qtextdocumentfragment.h +++ b/src/gui/text/qtextdocumentfragment.h @@ -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; diff --git a/tests/auto/gui/text/qtextcursor/tst_qtextcursor.cpp b/tests/auto/gui/text/qtextcursor/tst_qtextcursor.cpp index 17d2336b74..cea80efc3b 100644 --- a/tests/auto/gui/text/qtextcursor/tst_qtextcursor.cpp +++ b/tests/auto/gui/text/qtextcursor/tst_qtextcursor.cpp @@ -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"); |