summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShawn Rutledge <shawn.rutledge@qt.io>2017-12-19 15:25:55 +0100
committerShawn Rutledge <shawn.rutledge@qt.io>2019-05-01 14:31:27 +0000
commit23c2da3cc23a2e04a0b3b3c8ad7fa9cc6126ff23 (patch)
tree849770044988425cf290b8599b44440653bcddd0
parent9ec564b0bfc94c2d33f02b24ca081b64ff9ebb9b (diff)
Add QTextMarkdownWriter, QTextEdit::markdown property etc.
A QTextDocument can now be written out in Markdown format. - Add the QTextMarkdownWriter as a private class for now - Add QTextDocument::toMarkdown() - QTextDocumentWriter uses QTextMarkdownWriter if setFormat("markdown") is called or if the file suffix is .md or .mkd - Add QTextEdit::toMarkdown() and the markdown property [ChangeLog][QtGui][Text] Markdown (CommonMark or GitHub dialect) is now a supported format for reading into and writing from QTextDocument. Change-Id: I663a77017fac7ae1b3f9a400f5cd357bb40750af Reviewed-by: Gatis Paeglis <gatis.paeglis@qt.io>
-rw-r--r--src/gui/configure.json8
-rw-r--r--src/gui/text/qtextdocument.cpp34
-rw-r--r--src/gui/text/qtextdocument.h8
-rw-r--r--src/gui/text/qtextdocumentwriter.cpp19
-rw-r--r--src/gui/text/qtextmarkdownwriter.cpp363
-rw-r--r--src/gui/text/qtextmarkdownwriter_p.h78
-rw-r--r--src/gui/text/text.pri7
-rw-r--r--src/widgets/widgets/qtextedit.cpp62
-rw-r--r--src/widgets/widgets/qtextedit.h8
-rw-r--r--src/widgets/widgets/qwidgettextcontrol.cpp7
-rw-r--r--src/widgets/widgets/qwidgettextcontrol_p.h3
-rw-r--r--tests/auto/gui/text/qtextmarkdownwriter/BLACKLIST3
-rw-r--r--tests/auto/gui/text/qtextmarkdownwriter/data/example.md95
-rw-r--r--tests/auto/gui/text/qtextmarkdownwriter/qtextmarkdownwriter.pro7
-rw-r--r--tests/auto/gui/text/qtextmarkdownwriter/tst_qtextmarkdownwriter.cpp360
-rw-r--r--tests/auto/gui/text/text.pro3
-rw-r--r--tests/manual/markdown/html2md.cpp64
-rw-r--r--tests/manual/markdown/html2md.pro6
18 files changed, 1124 insertions, 11 deletions
diff --git a/src/gui/configure.json b/src/gui/configure.json
index d7c0da4640..9e76fc455e 100644
--- a/src/gui/configure.json
+++ b/src/gui/configure.json
@@ -1611,6 +1611,12 @@
"condition": "libs.libmd4c",
"output": [ "publicFeature" ]
},
+ "textmarkdownwriter": {
+ "label": "MarkdownWriter",
+ "purpose": "Provides a Markdown (CommonMark) writer",
+ "section": "Kernel",
+ "output": [ "publicFeature" ]
+ },
"textodfwriter": {
"label": "OdfWriter",
"purpose": "Provides an ODF writer.",
@@ -1892,7 +1898,7 @@ QMAKE_LIBDIR_OPENGL[_ES2] and QMAKE_LIBS_OPENGL[_ES2] in the mkspec for your pla
{
"section": "Text formats",
"entries": [
- "texthtmlparser", "cssparser", "textodfwriter", "textmarkdownreader", "system-textmarkdownreader"
+ "texthtmlparser", "cssparser", "textodfwriter", "textmarkdownreader", "system-textmarkdownreader", "textmarkdownwriter"
]
},
"egl",
diff --git a/src/gui/text/qtextdocument.cpp b/src/gui/text/qtextdocument.cpp
index 87c8f1ba8a..0a59bfb838 100644
--- a/src/gui/text/qtextdocument.cpp
+++ b/src/gui/text/qtextdocument.cpp
@@ -73,6 +73,9 @@
#if QT_CONFIG(textmarkdownreader)
#include <private/qtextmarkdownimporter_p.h>
#endif
+#if QT_CONFIG(textmarkdownwriter)
+#include <private/qtextmarkdownwriter_p.h>
+#endif
#include <limits.h>
@@ -3289,6 +3292,22 @@ QString QTextDocument::toHtml(const QByteArray &encoding) const
#endif // QT_NO_TEXTHTMLPARSER
/*!
+ Returns a string containing a Markdown representation of the document,
+ or an empty string if writing fails for any reason.
+*/
+#if QT_CONFIG(textmarkdownwriter)
+QString QTextDocument::toMarkdown(QTextDocument::MarkdownFeatures features) const
+{
+ QString ret;
+ QTextStream s(&ret);
+ QTextMarkdownWriter w(s, features);
+ if (w.writeAll(*this))
+ return ret;
+ return QString();
+}
+#endif
+
+/*!
Replaces the entire contents of the document with the given
Markdown-formatted text in the \a markdown string, with the given
\a features supported. By default, all supported GitHub-style
@@ -3301,8 +3320,19 @@ QString QTextDocument::toHtml(const QByteArray &encoding) const
Parsing of HTML included in the \a markdown string is handled in the same
way as in \l setHtml; however, Markdown formatting inside HTML blocks is
- not supported. The \c MarkdownNoHTML feature flag can be set to disable
- HTML parsing.
+ not supported.
+
+ Some features of the parser can be enabled or disabled via the \a features
+ argument:
+
+ \value MarkdownNoHTML
+ Any HTML tags in the Markdown text will be discarded
+ \value MarkdownDialectCommonMark
+ The parser supports only the features standardized by CommonMark
+ \value MarkdownDialectGitHub
+ The parser supports the GitHub dialect
+
+ The default is \c MarkdownDialectGitHub.
The undo/redo history is reset when this function is called.
*/
diff --git a/src/gui/text/qtextdocument.h b/src/gui/text/qtextdocument.h
index ade67999ad..31c06976a5 100644
--- a/src/gui/text/qtextdocument.h
+++ b/src/gui/text/qtextdocument.h
@@ -151,7 +151,7 @@ public:
void setHtml(const QString &html);
#endif
-#if QT_CONFIG(textmarkdownreader)
+#if QT_CONFIG(textmarkdownwriter) || QT_CONFIG(textmarkdownreader)
// Must be in sync with QTextMarkdownImporter::Features, should be in sync with #define MD_FLAG_* in md4c
enum MarkdownFeature {
MarkdownNoHTML = 0x0020 | 0x0040,
@@ -160,7 +160,13 @@ public:
};
Q_DECLARE_FLAGS(MarkdownFeatures, MarkdownFeature)
Q_FLAG(MarkdownFeatures)
+#endif
+#if QT_CONFIG(textmarkdownwriter)
+ QString toMarkdown(MarkdownFeatures features = MarkdownDialectGitHub) const;
+#endif
+
+#if QT_CONFIG(textmarkdownreader)
void setMarkdown(const QString &markdown, MarkdownFeatures features = MarkdownDialectGitHub);
#endif
diff --git a/src/gui/text/qtextdocumentwriter.cpp b/src/gui/text/qtextdocumentwriter.cpp
index 42e623153a..c82ff873cd 100644
--- a/src/gui/text/qtextdocumentwriter.cpp
+++ b/src/gui/text/qtextdocumentwriter.cpp
@@ -51,6 +51,9 @@
#include "qtextdocumentfragment_p.h"
#include "qtextodfwriter_p.h"
+#if QT_CONFIG(textmarkdownwriter)
+#include "qtextmarkdownwriter_p.h"
+#endif
#include <algorithm>
@@ -267,6 +270,18 @@ bool QTextDocumentWriter::write(const QTextDocument *document)
}
#endif // QT_NO_TEXTODFWRITER
+#if QT_CONFIG(textmarkdownwriter)
+ if (format == "md" || format == "mkd" || format == "markdown") {
+ if (!d->device->isWritable() && !d->device->open(QIODevice::WriteOnly)) {
+ qWarning("QTextDocumentWriter::write: the device can not be opened for writing");
+ return false;
+ }
+ QTextStream s(d->device);
+ QTextMarkdownWriter writer(s, QTextDocument::MarkdownDialectGitHub);
+ return writer.writeAll(*document);
+ }
+#endif // textmarkdownwriter
+
#ifndef QT_NO_TEXTHTMLPARSER
if (format == "html" || format == "htm") {
if (!d->device->isWritable() && ! d->device->open(QIODevice::WriteOnly)) {
@@ -348,6 +363,7 @@ QTextCodec *QTextDocumentWriter::codec() const
\header \li Format \li Description
\row \li plaintext \li Plain text
\row \li HTML \li HyperText Markup Language
+ \row \li markdown \li Markdown (CommonMark or GitHub dialects)
\row \li ODF \li OpenDocument Format
\endtable
@@ -364,6 +380,9 @@ QList<QByteArray> QTextDocumentWriter::supportedDocumentFormats()
#ifndef QT_NO_TEXTODFWRITER
answer << "ODF";
#endif // QT_NO_TEXTODFWRITER
+#if QT_CONFIG(textmarkdownwriter)
+ answer << "markdown";
+#endif
std::sort(answer.begin(), answer.end());
return answer;
diff --git a/src/gui/text/qtextmarkdownwriter.cpp b/src/gui/text/qtextmarkdownwriter.cpp
new file mode 100644
index 0000000000..c91248757a
--- /dev/null
+++ b/src/gui/text/qtextmarkdownwriter.cpp
@@ -0,0 +1,363 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtGui module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "qtextmarkdownwriter_p.h"
+#include "qtextdocumentlayout_p.h"
+#include "qfontinfo.h"
+#include "qfontmetrics.h"
+#include "qtextdocument_p.h"
+#include "qtextlist.h"
+#include "qtexttable.h"
+#include "qtextcursor.h"
+#include "qtextimagehandler_p.h"
+
+QT_BEGIN_NAMESPACE
+
+static const QChar Space = QLatin1Char(' ');
+static const QChar Newline = QLatin1Char('\n');
+static const QChar Backtick = QLatin1Char('`');
+
+QTextMarkdownWriter::QTextMarkdownWriter(QTextStream &stream, QTextDocument::MarkdownFeatures features)
+ : m_stream(stream), m_features(features)
+{
+}
+
+bool QTextMarkdownWriter::writeAll(const QTextDocument &document)
+{
+ writeFrame(document.rootFrame());
+ return true;
+}
+
+void QTextMarkdownWriter::writeFrame(const QTextFrame *frame)
+{
+ Q_ASSERT(frame);
+ const QTextTable *table = qobject_cast<const QTextTable*> (frame);
+ QTextFrame::iterator iterator = frame->begin();
+ QTextFrame *child = 0;
+ int tableRow = -1;
+ bool lastWasList = false;
+ QVector<int> tableColumnWidths;
+ if (table) {
+ tableColumnWidths.resize(table->columns());
+ for (int col = 0; col < table->columns(); ++col) {
+ for (int row = 0; row < table->rows(); ++ row) {
+ QTextTableCell cell = table->cellAt(row, col);
+ int cellTextLen = 0;
+ auto it = cell.begin();
+ while (it != cell.end()) {
+ QTextBlock block = it.currentBlock();
+ if (block.isValid())
+ cellTextLen += block.text().length();
+ ++it;
+ }
+ if (cell.columnSpan() == 1 && tableColumnWidths[col] < cellTextLen)
+ tableColumnWidths[col] = cellTextLen;
+ }
+ }
+ }
+ while (!iterator.atEnd()) {
+ if (iterator.currentFrame() && child != iterator.currentFrame())
+ writeFrame(iterator.currentFrame());
+ else { // no frame, it's a block
+ QTextBlock block = iterator.currentBlock();
+ if (table) {
+ QTextTableCell cell = table->cellAt(block.position());
+ if (tableRow < cell.row()) {
+ if (tableRow == 0) {
+ m_stream << Newline;
+ for (int col = 0; col < tableColumnWidths.length(); ++col)
+ m_stream << '|' << QString(tableColumnWidths[col], QLatin1Char('-'));
+ m_stream << '|';
+ }
+ m_stream << Newline << "|";
+ tableRow = cell.row();
+ }
+ } else if (!block.textList()) {
+ if (lastWasList)
+ m_stream << Newline;
+ }
+ int endingCol = writeBlock(block, !table, table && tableRow == 0);
+ if (table) {
+ QTextTableCell cell = table->cellAt(block.position());
+ int paddingLen = -endingCol;
+ int spanEndCol = cell.column() + cell.columnSpan();
+ for (int col = cell.column(); col < spanEndCol; ++col)
+ paddingLen += tableColumnWidths[col];
+ if (paddingLen > 0)
+ m_stream << QString(paddingLen, Space);
+ for (int col = cell.column(); col < spanEndCol; ++col)
+ m_stream << "|";
+ } else if (block.textList()) {
+ m_stream << Newline;
+ } else if (endingCol > 0) {
+ m_stream << Newline << Newline;
+ }
+ lastWasList = block.textList();
+ }
+ child = iterator.currentFrame();
+ ++iterator;
+ }
+ if (table)
+ m_stream << Newline << Newline;
+}
+
+static int nearestWordWrapIndex(const QString &s, int before)
+{
+ before = qMin(before, s.length());
+ for (int i = before - 1; i >= 0; --i) {
+ if (s.at(i).isSpace())
+ return i;
+ }
+ return -1;
+}
+
+static int adjacentBackticksCount(const QString &s)
+{
+ int start = -1, len = s.length();
+ int ret = 0;
+ for (int i = 0; i < len; ++i) {
+ if (s.at(i) == Backtick) {
+ if (start < 0)
+ start = i;
+ } else if (start >= 0) {
+ ret = qMax(ret, i - start);
+ start = -1;
+ }
+ }
+ if (s.at(len - 1) == Backtick)
+ ret = qMax(ret, len - start);
+ return ret;
+}
+
+static void maybeEscapeFirstChar(QString &s)
+{
+ QString sTrimmed = s.trimmed();
+ if (sTrimmed.isEmpty())
+ return;
+ char firstChar = sTrimmed.at(0).toLatin1();
+ if (firstChar == '*' || firstChar == '+' || firstChar == '-') {
+ int i = s.indexOf(QLatin1Char(firstChar));
+ s.insert(i, QLatin1Char('\\'));
+ }
+}
+
+int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ignoreFormat)
+{
+ int ColumnLimit = 80;
+ int wrapIndent = 0;
+ if (block.textList()) { // it's a list-item
+ auto fmt = block.textList()->format();
+ const int listLevel = fmt.indent();
+ const int number = block.textList()->itemNumber(block) + 1;
+ QByteArray bullet = " ";
+ bool numeric = false;
+ switch (fmt.style()) {
+ case QTextListFormat::ListDisc: bullet = "-"; break;
+ case QTextListFormat::ListCircle: bullet = "*"; break;
+ case QTextListFormat::ListSquare: bullet = "+"; break;
+ case QTextListFormat::ListStyleUndefined: break;
+ case QTextListFormat::ListDecimal:
+ case QTextListFormat::ListLowerAlpha:
+ case QTextListFormat::ListUpperAlpha:
+ case QTextListFormat::ListLowerRoman:
+ case QTextListFormat::ListUpperRoman:
+ numeric = true;
+ break;
+ }
+ switch (block.blockFormat().marker()) {
+ case QTextBlockFormat::Checked:
+ bullet += " [x]";
+ break;
+ case QTextBlockFormat::Unchecked:
+ bullet += " [ ]";
+ break;
+ default:
+ break;
+ }
+ QString prefix((listLevel - 1) * (numeric ? 4 : 2), Space);
+ if (numeric)
+ prefix += QString::number(number) + fmt.numberSuffix() + Space;
+ else
+ prefix += QLatin1String(bullet) + Space;
+ m_stream << prefix;
+ wrapIndent = prefix.length();
+ }
+
+ if (block.blockFormat().headingLevel())
+ m_stream << QByteArray(block.blockFormat().headingLevel(), '#') << ' ';
+
+ QString wrapIndentString(wrapIndent, Space);
+ // It would be convenient if QTextStream had a lineCharPos() accessor,
+ // to keep track of how many characters (not bytes) have been written on the current line,
+ // but it doesn't. So we have to keep track with this col variable.
+ int col = wrapIndent;
+ bool mono = false;
+ bool startsOrEndsWithBacktick = false;
+ bool bold = false;
+ bool italic = false;
+ bool underline = false;
+ bool strikeOut = false;
+ QString backticks(Backtick);
+ for (QTextBlock::Iterator frag = block.begin(); !frag.atEnd(); ++frag) {
+ QString fragmentText = frag.fragment().text();
+ while (fragmentText.endsWith(QLatin1Char('\n')))
+ fragmentText.chop(1);
+ startsOrEndsWithBacktick |= fragmentText.startsWith(Backtick) || fragmentText.endsWith(Backtick);
+ QTextCharFormat fmt = frag.fragment().charFormat();
+ if (fmt.isImageFormat()) {
+ QTextImageFormat ifmt = fmt.toImageFormat();
+ QString s = QLatin1String("![image](") + ifmt.name() + QLatin1Char(')');
+ if (wrap && col + s.length() > ColumnLimit) {
+ m_stream << Newline << wrapIndentString;
+ col = wrapIndent;
+ }
+ m_stream << s;
+ col += s.length();
+ } else if (fmt.hasProperty(QTextFormat::AnchorHref)) {
+ QString s = QLatin1Char('[') + fragmentText + QLatin1String("](") +
+ fmt.property(QTextFormat::AnchorHref).toString() + QLatin1Char(')');
+ if (wrap && col + s.length() > ColumnLimit) {
+ m_stream << Newline << wrapIndentString;
+ col = wrapIndent;
+ }
+ m_stream << s;
+ col += s.length();
+ } else {
+ QFontInfo fontInfo(fmt.font());
+ bool monoFrag = fontInfo.fixedPitch();
+ QString markers;
+ if (!ignoreFormat) {
+ if (monoFrag != mono) {
+ if (monoFrag)
+ backticks = QString::fromLatin1(QByteArray(adjacentBackticksCount(fragmentText) + 1, '`'));
+ markers += backticks;
+ if (startsOrEndsWithBacktick)
+ markers += Space;
+ mono = monoFrag;
+ }
+ if (!block.blockFormat().headingLevel() && !mono) {
+ if (fmt.font().bold() != bold) {
+ markers += QLatin1String("**");
+ bold = fmt.font().bold();
+ }
+ if (fmt.font().italic() != italic) {
+ markers += QLatin1Char('*');
+ italic = fmt.font().italic();
+ }
+ if (fmt.font().strikeOut() != strikeOut) {
+ markers += QLatin1String("~~");
+ strikeOut = fmt.font().strikeOut();
+ }
+ if (fmt.font().underline() != underline) {
+ // Markdown doesn't support underline, but the parser will treat a single underline
+ // the same as a single asterisk, and the marked fragment will be rendered in italics.
+ // That will have to do.
+ markers += QLatin1Char('_');
+ underline = fmt.font().underline();
+ }
+ }
+ }
+ if (wrap && col + markers.length() * 2 + fragmentText.length() > ColumnLimit) {
+ int i = 0;
+ int fragLen = fragmentText.length();
+ bool breakingLine = false;
+ while (i < fragLen) {
+ int j = i + ColumnLimit - col;
+ if (j < fragLen) {
+ int wi = nearestWordWrapIndex(fragmentText, j);
+ if (wi < 0) {
+ j = fragLen;
+ } else {
+ j = wi;
+ breakingLine = true;
+ }
+ } else {
+ j = fragLen;
+ breakingLine = false;
+ }
+ QString subfrag = fragmentText.mid(i, j - i);
+ if (!i) {
+ m_stream << markers;
+ col += markers.length();
+ }
+ if (col == wrapIndent)
+ maybeEscapeFirstChar(subfrag);
+ m_stream << subfrag;
+ if (breakingLine) {
+ m_stream << Newline << wrapIndentString;
+ col = wrapIndent;
+ } else {
+ col += subfrag.length();
+ }
+ i = j + 1;
+ }
+ } else {
+ m_stream << markers << fragmentText;
+ col += markers.length() + fragmentText.length();
+ }
+ }
+ }
+ if (mono) {
+ if (startsOrEndsWithBacktick) {
+ m_stream << Space;
+ col += 1;
+ }
+ m_stream << backticks;
+ col += backticks.size();
+ }
+ if (bold) {
+ m_stream << "**";
+ col += 2;
+ }
+ if (italic) {
+ m_stream << "*";
+ col += 1;
+ }
+ if (underline) {
+ m_stream << "_";
+ col += 1;
+ }
+ if (strikeOut) {
+ m_stream << "~~";
+ col += 2;
+ }
+ return col;
+}
+
+QT_END_NAMESPACE
diff --git a/src/gui/text/qtextmarkdownwriter_p.h b/src/gui/text/qtextmarkdownwriter_p.h
new file mode 100644
index 0000000000..9845355259
--- /dev/null
+++ b/src/gui/text/qtextmarkdownwriter_p.h
@@ -0,0 +1,78 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtGui module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QTEXTMARKDOWNWRITER_P_H
+#define QTEXTMARKDOWNWRITER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtGui/private/qtguiglobal_p.h>
+#include <QtCore/QTextStream>
+
+#include "qtextdocument_p.h"
+#include "qtextdocumentwriter.h"
+
+QT_BEGIN_NAMESPACE
+
+class Q_GUI_EXPORT QTextMarkdownWriter
+{
+public:
+ QTextMarkdownWriter(QTextStream &stream, QTextDocument::MarkdownFeatures features);
+ bool writeAll(const QTextDocument &document);
+
+ int writeBlock(const QTextBlock &block, bool table, bool ignoreFormat);
+ void writeFrame(const QTextFrame *frame);
+
+private:
+ QTextStream &m_stream;
+ QTextDocument::MarkdownFeatures m_features;
+};
+
+QT_END_NAMESPACE
+
+#endif // QTEXTMARKDOWNWRITER_P_H
diff --git a/src/gui/text/text.pri b/src/gui/text/text.pri
index b35a231747..5e97b312f1 100644
--- a/src/gui/text/text.pri
+++ b/src/gui/text/text.pri
@@ -109,6 +109,13 @@ qtConfig(textmarkdownreader) {
text/qtextmarkdownimporter.cpp
}
+qtConfig(textmarkdownwriter) {
+ HEADERS += \
+ text/qtextmarkdownwriter_p.h
+ SOURCES += \
+ text/qtextmarkdownwriter.cpp
+}
+
qtConfig(cssparser) {
HEADERS += \
text/qcssparser_p.h
diff --git a/src/widgets/widgets/qtextedit.cpp b/src/widgets/widgets/qtextedit.cpp
index 9e134493b5..5f734258b2 100644
--- a/src/widgets/widgets/qtextedit.cpp
+++ b/src/widgets/widgets/qtextedit.cpp
@@ -366,8 +366,8 @@ void QTextEditPrivate::_q_ensureVisible(const QRectF &_rect)
\section1 Introduction and Concepts
QTextEdit is an advanced WYSIWYG viewer/editor supporting rich
- text formatting using HTML-style tags. It is optimized to handle
- large documents and to respond quickly to user input.
+ text formatting using HTML-style tags, or Markdown format. It is optimized
+ to handle large documents and to respond quickly to user input.
QTextEdit works on paragraphs and characters. A paragraph is a
formatted string which is word-wrapped to fit into the width of
@@ -381,7 +381,7 @@ void QTextEditPrivate::_q_ensureVisible(const QRectF &_rect)
QTextEdit can display images, lists and tables. If the text is
too large to view within the text edit's viewport, scroll bars will
appear. The text edit can load both plain text and rich text files.
- Rich text is described using a subset of HTML 4 markup, refer to the
+ Rich text can be described using a subset of HTML 4 markup; refer to the
\l {Supported HTML Subset} page for more information.
If you just need to display a small piece of rich text use QLabel.
@@ -401,12 +401,19 @@ void QTextEditPrivate::_q_ensureVisible(const QRectF &_rect)
QTextEdit can display a large HTML subset, including tables and
images.
- The text is set or replaced using setHtml() which deletes any
+ The text can be set or replaced using \l setHtml() which deletes any
existing text and replaces it with the text passed in the
setHtml() call. If you call setHtml() with legacy HTML, and then
call toHtml(), the text that is returned may have different markup,
but will render the same. The entire text can be deleted with clear().
+ Text can also be set or replaced using \l setMarkdown(), and the same
+ caveats apply: if you then call \l toMarkdown(), the text that is returned
+ may be different, but the meaning is preserved as much as possible.
+ Markdown with some embedded HTML can be parsed, with the same limitations
+ that \l setHtml() has; but \l toMarkdown() only writes "pure" Markdown,
+ without any embedded HTML.
+
Text itself can be inserted using the QTextCursor class or using the
convenience functions insertHtml(), insertPlainText(), append() or
paste(). QTextCursor is also able to insert complex objects like tables
@@ -1213,11 +1220,54 @@ QString QTextEdit::toHtml() const
}
#endif
+#if QT_CONFIG(textmarkdownreader) && QT_CONFIG(textmarkdownwriter)
+/*!
+ \property QTextEdit::markdown
+
+ This property provides a Markdown interface to the text of the text edit.
+
+ \c toMarkdown() returns the text of the text edit as "pure" Markdown,
+ without any embedded HTML formatting. Some features that QTextDocument
+ supports (such as the use of specific colors and named fonts) cannot be
+ expressed in "pure" Markdown, and they will be omitted.
+
+ \c setMarkdown() changes the text of the text edit. Any previous text is
+ removed and the undo/redo history is cleared. The input text is
+ interpreted as rich text in Markdown format.
+
+ Parsing of HTML included in the \a markdown string is handled in the same
+ way as in \l setHtml; however, Markdown formatting inside HTML blocks is
+ not supported.
+
+ Some features of the parser can be enabled or disabled via the \a features
+ argument:
+
+ \value MarkdownNoHTML
+ Any HTML tags in the Markdown text will be discarded
+ \value MarkdownDialectCommonMark
+ The parser supports only the features standardized by CommonMark
+ \value MarkdownDialectGitHub
+ The parser supports the GitHub dialect
+
+ The default is \c MarkdownDialectGitHub.
+
+ \sa plainText, html, QTextDocument::toMarkdown(), QTextDocument::setMarkdown()
+*/
+#endif
+
#if QT_CONFIG(textmarkdownreader)
-void QTextEdit::setMarkdown(const QString &text)
+void QTextEdit::setMarkdown(const QString &markdown)
+{
+ Q_D(const QTextEdit);
+ d->control->setMarkdown(markdown);
+}
+#endif
+
+#if QT_CONFIG(textmarkdownwriter)
+QString QTextEdit::toMarkdown(QTextDocument::MarkdownFeatures features) const
{
Q_D(const QTextEdit);
- d->control->setMarkdown(text);
+ return d->control->toMarkdown(features);
}
#endif
diff --git a/src/widgets/widgets/qtextedit.h b/src/widgets/widgets/qtextedit.h
index f20bd936c4..3b7e610786 100644
--- a/src/widgets/widgets/qtextedit.h
+++ b/src/widgets/widgets/qtextedit.h
@@ -71,6 +71,9 @@ class Q_WIDGETS_EXPORT QTextEdit : public QAbstractScrollArea
QDOC_PROPERTY(QTextOption::WrapMode wordWrapMode READ wordWrapMode WRITE setWordWrapMode)
Q_PROPERTY(int lineWrapColumnOrWidth READ lineWrapColumnOrWidth WRITE setLineWrapColumnOrWidth)
Q_PROPERTY(bool readOnly READ isReadOnly WRITE setReadOnly)
+#if QT_CONFIG(textmarkdownreader) && QT_CONFIG(textmarkdownwriter)
+ Q_PROPERTY(QString markdown READ toMarkdown WRITE setMarkdown NOTIFY textChanged)
+#endif
#ifndef QT_NO_TEXTHTMLPARSER
Q_PROPERTY(QString html READ toHtml WRITE setHtml NOTIFY textChanged USER true)
#endif
@@ -174,6 +177,9 @@ public:
#ifndef QT_NO_TEXTHTMLPARSER
QString toHtml() const;
#endif
+#if QT_CONFIG(textmarkdownwriter)
+ QString toMarkdown(QTextDocument::MarkdownFeatures features = QTextDocument::MarkdownDialectGitHub) const;
+#endif
void ensureCursorVisible();
@@ -239,7 +245,7 @@ public Q_SLOTS:
void setHtml(const QString &text);
#endif
#if QT_CONFIG(textmarkdownreader)
- void setMarkdown(const QString &text);
+ void setMarkdown(const QString &markdown);
#endif
void setText(const QString &text);
diff --git a/src/widgets/widgets/qwidgettextcontrol.cpp b/src/widgets/widgets/qwidgettextcontrol.cpp
index 5744d43cbf..209156b901 100644
--- a/src/widgets/widgets/qwidgettextcontrol.cpp
+++ b/src/widgets/widgets/qwidgettextcontrol.cpp
@@ -3130,6 +3130,13 @@ QString QWidgetTextControl::toHtml() const
}
#endif
+#ifndef QT_NO_TEXTHTMLPARSER
+QString QWidgetTextControl::toMarkdown(QTextDocument::MarkdownFeatures features) const
+{
+ return document()->toMarkdown(features);
+}
+#endif
+
void QWidgetTextControlPrivate::append(const QString &text, Qt::TextFormat format)
{
QTextCursor tmp(doc);
diff --git a/src/widgets/widgets/qwidgettextcontrol_p.h b/src/widgets/widgets/qwidgettextcontrol_p.h
index 4c9e47dfc9..e521e7b356 100644
--- a/src/widgets/widgets/qwidgettextcontrol_p.h
+++ b/src/widgets/widgets/qwidgettextcontrol_p.h
@@ -128,6 +128,9 @@ public:
#ifndef QT_NO_TEXTHTMLPARSER
QString toHtml() const;
#endif
+#if QT_CONFIG(textmarkdownwriter)
+ QString toMarkdown(QTextDocument::MarkdownFeatures features = QTextDocument::MarkdownDialectGitHub) const;
+#endif
virtual void ensureCursorVisible();
diff --git a/tests/auto/gui/text/qtextmarkdownwriter/BLACKLIST b/tests/auto/gui/text/qtextmarkdownwriter/BLACKLIST
new file mode 100644
index 0000000000..fc9e5a9efe
--- /dev/null
+++ b/tests/auto/gui/text/qtextmarkdownwriter/BLACKLIST
@@ -0,0 +1,3 @@
+[rewriteDocument]
+winrt # QTBUG-54623
+
diff --git a/tests/auto/gui/text/qtextmarkdownwriter/data/example.md b/tests/auto/gui/text/qtextmarkdownwriter/data/example.md
new file mode 100644
index 0000000000..3c63f209a2
--- /dev/null
+++ b/tests/auto/gui/text/qtextmarkdownwriter/data/example.md
@@ -0,0 +1,95 @@
+# QTextEdit
+
+The QTextEdit widget is an advanced editor that supports formatted rich text.
+It can be used to display HTML and other rich document formats. Internally,
+QTextEdit uses the QTextDocument class to describe both the high-level
+structure of each document and the low-level formatting of paragraphs.
+
+If you are viewing this document in the textedit example, you can edit this
+document to explore Qt's rich text editing features. We have included some
+comments in each of the following sections to encourage you to experiment.
+
+## Font and Paragraph Styles
+
+QTextEdit supports **bold**, *italic*, and ~~strikethrough~~ font styles, and can
+display multicolored text. Font families such as Times New Roman and `Courier`
+can also be used directly. *If you place the cursor in a region of styled text,
+the controls in the tool bars will change to reflect the current style.*
+
+Paragraphs can be formatted so that the text is left-aligned, right-aligned,
+centered, or fully justified.
+
+*Try changing the alignment of some text and resize the editor to see how the
+text layout changes.*
+
+## Lists
+
+Different kinds of lists can be included in rich text documents. Standard
+bullet lists can be nested, using different symbols for each level of the list:
+
+* Disc symbols are typically used for top-level list items.
+ - Circle symbols can be used to distinguish between items in lower-level
+ lists.
+ + Square symbols provide a reasonable alternative to discs and circles.
+
+Ordered lists can be created that can be used for tables of contents. Different
+characters can be used to enumerate items, and we can use both Roman and Arabic
+numerals in the same list structure:
+
+1. Introduction
+2. Qt Tools
+ 1) Qt Assistant
+ 2) Qt Designer
+ 1. Form Editor
+ 2. Component Architecture
+ 3) Qt Linguist
+
+The list will automatically be renumbered if you add or remove items. *Try
+adding new sections to the above list or removing existing item to see the
+numbers change.*
+
+## Images
+
+Inline images are treated like ordinary ranges of characters in the text
+editor, so they flow with the surrounding text. Images can also be selected in
+the same way as text, making it easy to cut, copy, and paste them.
+
+![image](images/logo32.png) *Try to select this image by clicking and dragging
+over it with the mouse, or use the text cursor to select it by holding down
+Shift and using the arrow keys. You can then cut or copy it, and paste it into
+different parts of this document.*
+
+## Tables
+
+QTextEdit can arrange and format tables, supporting features such as row and
+column spans, text formatting within cells, and size constraints for columns.
+
+
+| |Development Tools |Programming Techniques |Graphical User Interfaces|
+|-------------|------------------------------------|---------------------------|-------------------------|
+|9:00 - 11:00 |Introduction to Qt |||
+|11:00 - 13:00|Using qmake |Object-oriented Programming|Layouts in Qt |
+|13:00 - 15:00|Qt Designer Tutorial |Extreme Programming |Writing Custom Styles |
+|15:00 - 17:00|Qt Linguist and Internationalization|  |  |
+
+*Try adding text to the cells in the table and experiment with the alignment of
+the paragraphs.*
+
+## Hyperlinks
+
+QTextEdit is designed to support hyperlinks between documents, and this feature
+is used extensively in
+[Qt Assistant](http://doc.qt.io/qt-5/qtassistant-index.html). Hyperlinks are
+automatically created when an HTML file is imported into an editor. Since the
+rich text framework supports hyperlinks natively, they can also be created
+programatically.
+
+## Undo and Redo
+
+Full support for undo and redo operations is built into QTextEdit and the
+underlying rich text framework. Operations on a document can be packaged
+together to make editing a more comfortable experience for the user.
+
+*Try making changes to this document and press `Ctrl+Z` to undo them. You can
+always recover the original contents of the document.*
+
diff --git a/tests/auto/gui/text/qtextmarkdownwriter/qtextmarkdownwriter.pro b/tests/auto/gui/text/qtextmarkdownwriter/qtextmarkdownwriter.pro
new file mode 100644
index 0000000000..04cf7ef5dd
--- /dev/null
+++ b/tests/auto/gui/text/qtextmarkdownwriter/qtextmarkdownwriter.pro
@@ -0,0 +1,7 @@
+CONFIG += testcase
+TARGET = tst_qtextmarkdownwriter
+QT += core-private gui-private testlib
+SOURCES += tst_qtextmarkdownwriter.cpp
+TESTDATA += data/example.md
+
+DEFINES += SRCDIR=\\\"$$PWD\\\"
diff --git a/tests/auto/gui/text/qtextmarkdownwriter/tst_qtextmarkdownwriter.cpp b/tests/auto/gui/text/qtextmarkdownwriter/tst_qtextmarkdownwriter.cpp
new file mode 100644
index 0000000000..bf7c9708de
--- /dev/null
+++ b/tests/auto/gui/text/qtextmarkdownwriter/tst_qtextmarkdownwriter.cpp
@@ -0,0 +1,360 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the test suite of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtTest/QtTest>
+#include <QTextDocument>
+#include <QTextCursor>
+#include <QTextBlock>
+#include <QTextList>
+#include <QTextTable>
+#include <QBuffer>
+#include <QDebug>
+
+#include <private/qtextmarkdownwriter_p.h>
+
+// #define DEBUG_WRITE_OUTPUT
+
+class tst_QTextMarkdownWriter : public QObject
+{
+ Q_OBJECT
+public slots:
+ void init();
+ void cleanup();
+
+private slots:
+ void testWriteParagraph_data();
+ void testWriteParagraph();
+ void testWriteList();
+ void testWriteNestedBulletLists();
+ void testWriteNestedNumericLists();
+ void testWriteTable();
+ void rewriteDocument();
+ void fromHtml_data();
+ void fromHtml();
+
+private:
+ QString documentToUnixMarkdown();
+
+private:
+ QTextDocument *document;
+};
+
+void tst_QTextMarkdownWriter::init()
+{
+ document = new QTextDocument();
+}
+
+void tst_QTextMarkdownWriter::cleanup()
+{
+ delete document;
+}
+
+void tst_QTextMarkdownWriter::testWriteParagraph_data()
+{
+ QTest::addColumn<QString>("input");
+ QTest::addColumn<QString>("output");
+
+ QTest::newRow("empty") << "" <<
+ "";
+ QTest::newRow("spaces") << "foobar word" <<
+ "foobar word\n\n";
+ QTest::newRow("starting spaces") << " starting spaces" <<
+ " starting spaces\n\n";
+ QTest::newRow("trailing spaces") << "trailing spaces " <<
+ "trailing spaces \n\n";
+ QTest::newRow("tab") << "word\ttab x" <<
+ "word\ttab x\n\n";
+ QTest::newRow("tab2") << "word\t\ttab\tx" <<
+ "word\t\ttab\tx\n\n";
+ QTest::newRow("misc") << "foobar word\ttab x" <<
+ "foobar word\ttab x\n\n";
+ QTest::newRow("misc2") << "\t \tFoo" <<
+ "\t \tFoo\n\n";
+}
+
+void tst_QTextMarkdownWriter::testWriteParagraph()
+{
+ QFETCH(QString, input);
+ QFETCH(QString, output);
+
+ QTextCursor cursor(document);
+ cursor.insertText(input);
+
+ QCOMPARE(documentToUnixMarkdown(), output);
+}
+
+void tst_QTextMarkdownWriter::testWriteList()
+{
+ QTextCursor cursor(document);
+ QTextList *list = cursor.createList(QTextListFormat::ListDisc);
+ cursor.insertText("ListItem 1");
+ list->add(cursor.block());
+ cursor.insertBlock();
+ cursor.insertText("ListItem 2");
+ list->add(cursor.block());
+
+ QCOMPARE(documentToUnixMarkdown(), QString::fromLatin1(
+ "- ListItem 1\n- ListItem 2\n"));
+}
+
+void tst_QTextMarkdownWriter::testWriteNestedBulletLists()
+{
+ QTextCursor cursor(document);
+
+ QTextList *list1 = cursor.createList(QTextListFormat::ListDisc);
+ cursor.insertText("ListItem 1");
+ list1->add(cursor.block());
+
+ QTextListFormat fmt2;
+ fmt2.setStyle(QTextListFormat::ListCircle);
+ fmt2.setIndent(2);
+ QTextList *list2 = cursor.insertList(fmt2);
+ cursor.insertText("ListItem 2");
+
+ QTextListFormat fmt3;
+ fmt3.setStyle(QTextListFormat::ListSquare);
+ fmt3.setIndent(3);
+ cursor.insertList(fmt3);
+ cursor.insertText("ListItem 3");
+
+ cursor.insertBlock();
+ cursor.insertText("ListItem 4");
+ list1->add(cursor.block());
+
+ cursor.insertBlock();
+ cursor.insertText("ListItem 5");
+ list2->add(cursor.block());
+
+ QCOMPARE(documentToUnixMarkdown(), QString::fromLatin1(
+ "- ListItem 1\n * ListItem 2\n + ListItem 3\n- ListItem 4\n * ListItem 5\n"));
+}
+
+void tst_QTextMarkdownWriter::testWriteNestedNumericLists()
+{
+ QTextCursor cursor(document);
+
+ QTextList *list1 = cursor.createList(QTextListFormat::ListDecimal);
+ cursor.insertText("ListItem 1");
+ list1->add(cursor.block());
+
+ QTextListFormat fmt2;
+ fmt2.setStyle(QTextListFormat::ListLowerAlpha);
+ fmt2.setNumberSuffix(QLatin1String(")"));
+ fmt2.setIndent(2);
+ QTextList *list2 = cursor.insertList(fmt2);
+ cursor.insertText("ListItem 2");
+
+ QTextListFormat fmt3;
+ fmt3.setStyle(QTextListFormat::ListDecimal);
+ fmt3.setIndent(3);
+ cursor.insertList(fmt3);
+ cursor.insertText("ListItem 3");
+
+ cursor.insertBlock();
+ cursor.insertText("ListItem 4");
+ list1->add(cursor.block());
+
+ cursor.insertBlock();
+ cursor.insertText("ListItem 5");
+ list2->add(cursor.block());
+
+ // There's no QTextList API to set the starting number so we hard-coded all lists to start at 1 (QTBUG-65384)
+ QCOMPARE(documentToUnixMarkdown(), QString::fromLatin1(
+ "1 ListItem 1\n 1) ListItem 2\n 1 ListItem 3\n2 ListItem 4\n 2) ListItem 5\n"));
+}
+
+void tst_QTextMarkdownWriter::testWriteTable()
+{
+ QTextCursor cursor(document);
+ QTextTable * table = cursor.insertTable(4, 3);
+ cursor = table->cellAt(0, 0).firstCursorPosition();
+ // valid Markdown tables need headers, but QTextTable doesn't make that distinction
+ // so QTextMarkdownWriter assumes the first row of any table is a header
+ cursor.insertText("one");
+ cursor.movePosition(QTextCursor::NextCell);
+ cursor.insertText("two");
+ cursor.movePosition(QTextCursor::NextCell);
+ cursor.insertText("three");
+ cursor.movePosition(QTextCursor::NextCell);
+
+ cursor.insertText("alice");
+ cursor.movePosition(QTextCursor::NextCell);
+ cursor.insertText("bob");
+ cursor.movePosition(QTextCursor::NextCell);
+ cursor.insertText("carl");
+ cursor.movePosition(QTextCursor::NextCell);
+
+ cursor.insertText("dennis");
+ cursor.movePosition(QTextCursor::NextCell);
+ cursor.insertText("eric");
+ cursor.movePosition(QTextCursor::NextCell);
+ cursor.insertText("fiona");
+ cursor.movePosition(QTextCursor::NextCell);
+
+ cursor.insertText("gina");
+ /*
+ |one |two |three|
+ |------|----|-----|
+ |alice |bob |carl |
+ |dennis|eric|fiona|
+ |gina | | |
+ */
+
+ QString md = documentToUnixMarkdown();
+
+#ifdef DEBUG_WRITE_OUTPUT
+ {
+ QFile out("/tmp/table.md");
+ out.open(QFile::WriteOnly);
+ out.write(md.toUtf8());
+ out.close();
+ }
+#endif
+
+ QString expected = QString::fromLatin1(
+ "\n|one |two |three|\n|------|----|-----|\n|alice |bob |carl |\n|dennis|eric|fiona|\n|gina | | |\n\n");
+ QCOMPARE(md, expected);
+
+ // create table with merged cells
+ document->clear();
+ cursor = QTextCursor(document);
+ table = cursor.insertTable(3, 3);
+ table->mergeCells(0, 0, 1, 2);
+ table->mergeCells(1, 1, 1, 2);
+ cursor = table->cellAt(0, 0).firstCursorPosition();
+ cursor.insertText("a");
+ cursor.movePosition(QTextCursor::NextCell);
+ cursor.insertText("b");
+ cursor.movePosition(QTextCursor::NextCell);
+ cursor.insertText("c");
+ cursor.movePosition(QTextCursor::NextCell);
+ cursor.insertText("d");
+ cursor.movePosition(QTextCursor::NextCell);
+ cursor.insertText("e");
+ cursor.movePosition(QTextCursor::NextCell);
+ cursor.insertText("f");
+ /*
+ +---+-+
+ |a |b|
+ +---+-+
+ |c| d|
+ +-+-+-+
+ |e|f| |
+ +-+-+-+
+
+ generates
+
+ |a ||b|
+ |-|-|-|
+ |c|d ||
+ |e|f| |
+
+ */
+
+ md = documentToUnixMarkdown();
+
+#ifdef DEBUG_WRITE_OUTPUT
+ {
+ QFile out("/tmp/table-merged-cells.md");
+ out.open(QFile::WriteOnly);
+ out.write(md.toUtf8());
+ out.close();
+ }
+#endif
+
+ QCOMPARE(md, QString::fromLatin1("\n|a ||b|\n|-|-|-|\n|c|d ||\n|e|f| |\n\n"));
+}
+
+void tst_QTextMarkdownWriter::rewriteDocument()
+{
+ QTextDocument doc;
+ QFile f(QFINDTESTDATA("data/example.md"));
+ QVERIFY(f.open(QFile::ReadOnly | QIODevice::Text));
+ QString orig = QString::fromUtf8(f.readAll());
+ f.close();
+ doc.setMarkdown(orig);
+ QString md = doc.toMarkdown();
+
+#ifdef DEBUG_WRITE_OUTPUT
+ QFile out("/tmp/rewrite.md");
+ out.open(QFile::WriteOnly);
+ out.write(md.toUtf8());
+ out.close();
+#endif
+
+ QCOMPARE(md, orig);
+}
+
+void tst_QTextMarkdownWriter::fromHtml_data()
+{
+ QTest::addColumn<QString>("input");
+ QTest::addColumn<QString>("output");
+
+ QTest::newRow("long URL") <<
+ "<span style=\"font-style:italic;\">https://www.example.com/dir/subdir/subsubdir/subsubsubdir/subsubsubsubdir/subsubsubsubsubdir/</span>" <<
+ "*https://www.example.com/dir/subdir/subsubdir/subsubsubdir/subsubsubsubdir/subsubsubsubsubdir/*\n\n";
+ QTest::newRow("non-emphasis inline asterisk") << "3 * 4" << "3 * 4\n\n";
+ QTest::newRow("arithmetic") << "(2 * a * x + b)^2 = b^2 - 4 * a * c" << "(2 * a * x + b)^2 = b^2 - 4 * a * c\n\n";
+ QTest::newRow("escaped asterisk after newline") <<
+ "The first sentence of this paragraph holds 80 characters, then there's a star. * This is wrapped, but is <em>not</em> a bullet point." <<
+ "The first sentence of this paragraph holds 80 characters, then there's a star.\n\\* This is wrapped, but is *not* a bullet point.\n\n";
+ QTest::newRow("escaped plus after newline") <<
+ "The first sentence of this paragraph holds 80 characters, then there's a plus. + This is wrapped, but is <em>not</em> a bullet point." <<
+ "The first sentence of this paragraph holds 80 characters, then there's a plus.\n\\+ This is wrapped, but is *not* a bullet point.\n\n";
+ QTest::newRow("escaped hyphen after newline") <<
+ "The first sentence of this paragraph holds 80 characters, then there's a minus. - This is wrapped, but is <em>not</em> a bullet point." <<
+ "The first sentence of this paragraph holds 80 characters, then there's a minus.\n\\- This is wrapped, but is *not* a bullet point.\n\n";
+ // TODO
+// QTest::newRow("escaped number and paren after double newline") <<
+// "<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";
+}
+
+void tst_QTextMarkdownWriter::fromHtml()
+{
+ QFETCH(QString, input);
+ QFETCH(QString, output);
+
+ document->setHtml(input);
+ QCOMPARE(documentToUnixMarkdown(), output);
+}
+
+QString tst_QTextMarkdownWriter::documentToUnixMarkdown()
+{
+ QString ret;
+ QTextStream ts(&ret, QIODevice::WriteOnly);
+ QTextMarkdownWriter writer(ts, QTextDocument::MarkdownDialectGitHub);
+ writer.writeAll(*document);
+ return ret;
+}
+
+QTEST_MAIN(tst_QTextMarkdownWriter)
+#include "tst_qtextmarkdownwriter.moc"
diff --git a/tests/auto/gui/text/text.pro b/tests/auto/gui/text/text.pro
index 6b033fb506..a98debe35c 100644
--- a/tests/auto/gui/text/text.pro
+++ b/tests/auto/gui/text/text.pro
@@ -28,12 +28,15 @@ SUBDIRS=\
win32:SUBDIRS -= qtextpiecetable
+qtConfig(textmarkdownwriter): SUBDIRS += qtextmarkdownwriter
+
!qtConfig(private_tests): SUBDIRS -= \
qfontcache \
qcssparser \
qtextlayout \
qtextpiecetable \
qzip \
+ qtextmarkdownwriter \
qtextodfwriter
!qtHaveModule(xml): SUBDIRS -= \
diff --git a/tests/manual/markdown/html2md.cpp b/tests/manual/markdown/html2md.cpp
new file mode 100644
index 0000000000..19d6ff06af
--- /dev/null
+++ b/tests/manual/markdown/html2md.cpp
@@ -0,0 +1,64 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2019 The Qt Company Ltd.
+ ** Contact: https://www.qt.io/licensing/
+ **
+ ** This file is part of the test suite of the Qt Toolkit.
+ **
+ ** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+ ** Commercial License Usage
+ ** Licensees holding valid commercial Qt licenses may use this file in
+ ** accordance with the commercial license agreement provided with the
+ ** Software or, alternatively, in accordance with the terms contained in
+ ** a written agreement between you and The Qt Company. For licensing terms
+ ** and conditions see https://www.qt.io/terms-conditions. For further
+ ** information use the contact form at https://www.qt.io/contact-us.
+ **
+ ** GNU General Public License Usage
+ ** Alternatively, this file may be used under the terms of the GNU
+ ** General Public License version 3 as published by the Free Software
+ ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+ ** included in the packaging of this file. Please review the following
+ ** information to ensure the GNU General Public License requirements will
+ ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+ **
+ ** $QT_END_LICENSE$
+ **
+ ****************************************************************************/
+
+#include <QCommandLineParser>
+#include <QDebug>
+#include <QFile>
+#include <QGuiApplication>
+#include <QTextDocument>
+
+int main(int argc, char **argv)
+{
+ QGuiApplication app(argc, argv);
+ QGuiApplication::setApplicationVersion(QT_VERSION_STR);
+ QCommandLineParser parser;
+ parser.setApplicationDescription("Converts the Qt-supported subset of HTML to Markdown.");
+ parser.addHelpOption();
+ parser.addVersionOption();
+ parser.addPositionalArgument(QGuiApplication::translate("main", "input"),
+ QGuiApplication::translate("main", "input file"));
+ parser.addPositionalArgument(QGuiApplication::translate("main", "output"),
+ QGuiApplication::translate("main", "output file"));
+ parser.process(app);
+ if (parser.positionalArguments().count() != 2)
+ parser.showHelp(1);
+
+ QFile inFile(parser.positionalArguments().first());
+ if (!inFile.open(QIODevice::ReadOnly)) {
+ qFatal("failed to open %s for reading", parser.positionalArguments().first().toLocal8Bit().data());
+ exit(2);
+ }
+ QFile outFile(parser.positionalArguments().at(1));
+ if (!outFile.open(QIODevice::WriteOnly)) {
+ qFatal("failed to open %s for writing", parser.positionalArguments().at(1).toLocal8Bit().data());
+ exit(2);
+ }
+ QTextDocument doc;
+ doc.setHtml(QString::fromUtf8(inFile.readAll()));
+ outFile.write(doc.toMarkdown().toUtf8());
+}
diff --git a/tests/manual/markdown/html2md.pro b/tests/manual/markdown/html2md.pro
new file mode 100644
index 0000000000..4d6254e5a0
--- /dev/null
+++ b/tests/manual/markdown/html2md.pro
@@ -0,0 +1,6 @@
+TEMPLATE = app
+TARGET = html2md
+INCLUDEPATH += .
+#QT += gui-private
+SOURCES += html2md.cpp
+