summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/gui/text/qtextcursor.cpp22
-rw-r--r--src/gui/text/qtextcursor.h7
-rw-r--r--src/gui/text/qtextdocumentfragment.cpp56
-rw-r--r--src/gui/text/qtextdocumentfragment.h9
-rw-r--r--tests/auto/gui/text/qtextcursor/tst_qtextcursor.cpp222
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");