summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShawn Rutledge <shawn.rutledge@qt.io>2017-12-18 08:55:18 +0100
committerShawn Rutledge <shawn.rutledge@qt.io>2019-04-17 15:26:54 +0000
commit65314b6ce88cdbb28a22be0cab9856ec9bc9604b (patch)
tree6f9e794face29ede4b7501027af20a2fb37f2e4c
parent860bf13dbd2e9ad0a87b75defcab2cc3c9c771f3 (diff)
Add QTextMarkdownImporter
This provides the ability to read from a Markdown string or file into a QTextDocument, such that the formatting will be recognized and can be rendered. - Add QTextDocument::setMarkdown(QString) - Add QTextEdit::setMarkdown(QString) - Add TextFormat::MarkdownText - QWidgetTextControl::setContent() calls QTextDocument::setMarkdown() if that's the format Fixes: QTBUG-72349 Change-Id: Ief2ad71bf840666c64145d58e9ca71d05fad5659 Reviewed-by: Lisandro Damián Nicanor Pérez Meyer <perezmeyer@gmail.com> Reviewed-by: Gatis Paeglis <gatis.paeglis@qt.io>
-rw-r--r--src/corelib/global/qnamespace.h3
-rw-r--r--src/gui/configure.json34
-rw-r--r--src/gui/text/qtextdocument.cpp30
-rw-r--r--src/gui/text/qtextdocument.h15
-rw-r--r--src/gui/text/qtextformat.h12
-rw-r--r--src/gui/text/qtextmarkdownimporter.cpp435
-rw-r--r--src/gui/text/qtextmarkdownimporter_p.h127
-rw-r--r--src/gui/text/text.pri12
-rw-r--r--src/widgets/widgets/qtextedit.cpp9
-rw-r--r--src/widgets/widgets/qtextedit.h5
-rw-r--r--src/widgets/widgets/qwidgettextcontrol.cpp15
-rw-r--r--src/widgets/widgets/qwidgettextcontrol_p.h5
-rw-r--r--src/widgets/widgets/qwidgettextcontrol_p_p.h2
13 files changed, 696 insertions, 8 deletions
diff --git a/src/corelib/global/qnamespace.h b/src/corelib/global/qnamespace.h
index 90dd36113f..06aef81afc 100644
--- a/src/corelib/global/qnamespace.h
+++ b/src/corelib/global/qnamespace.h
@@ -1198,7 +1198,8 @@ public:
enum TextFormat {
PlainText,
RichText,
- AutoText
+ AutoText,
+ MarkdownText
};
enum AspectRatioMode {
diff --git a/src/gui/configure.json b/src/gui/configure.json
index 7a1796041e..a4ea4375d1 100644
--- a/src/gui/configure.json
+++ b/src/gui/configure.json
@@ -28,6 +28,7 @@
"lgmon": "boolean",
"libinput": "boolean",
"libjpeg": { "type": "enum", "values": [ "no", "qt", "system" ] },
+ "libmd4c": { "type": "enum", "values": [ "no", "qt", "system" ] },
"libpng": { "type": "enum", "values": [ "no", "qt", "system" ] },
"linuxfb": "boolean",
"mtdev": "boolean",
@@ -376,6 +377,17 @@
"-ljpeg"
]
},
+ "libmd4c": {
+ "label": "libmd4c",
+ "test": {
+ "main": "md_parse(\"hello\", 5, nullptr, nullptr);"
+ },
+ "headers": "md4c.h",
+ "sources": [
+ { "type": "pkgConfig", "args": "md4c" },
+ { "libs": "-lmd4c" }
+ ]
+ },
"libpng": {
"label": "libpng",
"test": {
@@ -1583,6 +1595,22 @@
"section": "Kernel",
"output": [ "publicFeature", "feature" ]
},
+ "textmarkdownreader": {
+ "label": "MarkdownReader",
+ "disable": "input.libmd4c == 'no'",
+ "enable": "input.libmd4c == 'system' || input.libmd4c == 'qt' || input.libmd4c == 'yes'",
+ "purpose": "Provides a Markdown (CommonMark and GitHub) reader",
+ "section": "Kernel",
+ "output": [ "publicFeature" ]
+ },
+ "system-textmarkdownreader": {
+ "label": " Using system libmd4c",
+ "disable": "input.libmd4c == 'qt'",
+ "enable": "input.libmd4c == 'system'",
+ "section": "Kernel",
+ "condition": "libs.libmd4c",
+ "output": [ "publicFeature" ]
+ },
"textodfwriter": {
"label": "OdfWriter",
"purpose": "Provides an ODF writer.",
@@ -1861,6 +1889,12 @@ QMAKE_LIBDIR_OPENGL[_ES2] and QMAKE_LIBS_OPENGL[_ES2] in the mkspec for your pla
"gif", "ico", "jpeg", "system-jpeg", "png", "system-png"
]
},
+ {
+ "section": "Text formats",
+ "entries": [
+ "texthtmlparser", "cssparser", "textodfwriter", "textmarkdownreader", "system-textmarkdownreader"
+ ]
+ },
"egl",
"openvg",
{
diff --git a/src/gui/text/qtextdocument.cpp b/src/gui/text/qtextdocument.cpp
index fd3473b32e..87c8f1ba8a 100644
--- a/src/gui/text/qtextdocument.cpp
+++ b/src/gui/text/qtextdocument.cpp
@@ -1,6 +1,6 @@
/****************************************************************************
**
-** Copyright (C) 2016 The Qt Company Ltd.
+** 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.
@@ -70,6 +70,9 @@
#include <private/qabstracttextdocumentlayout_p.h>
#include "qpagedpaintdevice.h"
#include "private/qpagedpaintdevice_p.h"
+#if QT_CONFIG(textmarkdownreader)
+#include <private/qtextmarkdownimporter_p.h>
+#endif
#include <limits.h>
@@ -3286,6 +3289,31 @@ QString QTextDocument::toHtml(const QByteArray &encoding) const
#endif // QT_NO_TEXTHTMLPARSER
/*!
+ 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
+ Markdown features are included; pass \c MarkdownDialectCommonMark
+ for a more basic parse.
+
+ The Markdown formatting is respected as much as possible; for example,
+ "*bold* text" will produce text where the first word has a font weight that
+ gives it an emphasized appearance.
+
+ 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.
+
+ The undo/redo history is reset when this function is called.
+*/
+#if QT_CONFIG(textmarkdownreader)
+void QTextDocument::setMarkdown(const QString &markdown, QTextDocument::MarkdownFeatures features)
+{
+ QTextMarkdownImporter(static_cast<QTextMarkdownImporter::Features>(int(features))).import(this, markdown);
+}
+#endif
+
+/*!
Returns a vector of text formats for all the formats used in the document.
*/
QVector<QTextFormat> QTextDocument::allFormats() const
diff --git a/src/gui/text/qtextdocument.h b/src/gui/text/qtextdocument.h
index c9b22e053b..ade67999ad 100644
--- a/src/gui/text/qtextdocument.h
+++ b/src/gui/text/qtextdocument.h
@@ -1,6 +1,6 @@
/****************************************************************************
**
-** Copyright (C) 2016 The Qt Company Ltd.
+** 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.
@@ -151,6 +151,19 @@ public:
void setHtml(const QString &html);
#endif
+#if 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,
+ MarkdownDialectCommonMark = 0,
+ MarkdownDialectGitHub = 0x0004 | 0x0008 | 0x0400 | 0x0100 | 0x0200 | 0x0800
+ };
+ Q_DECLARE_FLAGS(MarkdownFeatures, MarkdownFeature)
+ Q_FLAG(MarkdownFeatures)
+
+ void setMarkdown(const QString &markdown, MarkdownFeatures features = MarkdownDialectGitHub);
+#endif
+
QString toRawText() const;
QString toPlainText() const;
void setPlainText(const QString &text);
diff --git a/src/gui/text/qtextformat.h b/src/gui/text/qtextformat.h
index 80d8e82694..1eb52a379c 100644
--- a/src/gui/text/qtextformat.h
+++ b/src/gui/text/qtextformat.h
@@ -176,6 +176,7 @@ public:
BlockNonBreakableLines = 0x1050,
BlockTrailingHorizontalRulerWidth = 0x1060,
HeadingLevel = 0x1070,
+ BlockMarker = 0x1080,
// character properties
FirstFontProperty = 0x1FE0,
@@ -605,6 +606,12 @@ public:
LineDistanceHeight = 4
};
+ enum MarkerType {
+ NoMarker = 0,
+ Unchecked = 1,
+ Checked = 2
+ };
+
QTextBlockFormat();
bool isValid() const { return isBlockFormat(); }
@@ -668,6 +675,11 @@ public:
void setTabPositions(const QList<QTextOption::Tab> &tabs);
QList<QTextOption::Tab> tabPositions() const;
+ inline void setMarker(MarkerType marker)
+ { setProperty(BlockMarker, int(marker)); }
+ inline MarkerType marker() const
+ { return MarkerType(intProperty(BlockMarker)); }
+
protected:
explicit QTextBlockFormat(const QTextFormat &fmt);
friend class QTextFormat;
diff --git a/src/gui/text/qtextmarkdownimporter.cpp b/src/gui/text/qtextmarkdownimporter.cpp
new file mode 100644
index 0000000000..6c053ac81a
--- /dev/null
+++ b/src/gui/text/qtextmarkdownimporter.cpp
@@ -0,0 +1,435 @@
+/****************************************************************************
+**
+** 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 "qtextmarkdownimporter_p.h"
+#include "qtextdocumentfragment_p.h"
+#include <QLoggingCategory>
+#include <QRegularExpression>
+#include <QTextCursor>
+#include <QTextDocument>
+#include <QTextDocumentFragment>
+#include <QTextList>
+#include <QTextTable>
+
+QT_BEGIN_NAMESPACE
+
+Q_LOGGING_CATEGORY(lcMD, "qt.text.markdown")
+
+// --------------------------------------------------------
+// MD4C callback function wrappers
+
+static int CbEnterBlock(MD_BLOCKTYPE type, void *detail, void *userdata)
+{
+ QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata);
+ return mdi->cbEnterBlock(type, detail);
+}
+
+static int CbLeaveBlock(MD_BLOCKTYPE type, void *detail, void *userdata)
+{
+ QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata);
+ return mdi->cbLeaveBlock(type, detail);
+}
+
+static int CbEnterSpan(MD_SPANTYPE type, void *detail, void *userdata)
+{
+ QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata);
+ return mdi->cbEnterSpan(type, detail);
+}
+
+static int CbLeaveSpan(MD_SPANTYPE type, void *detail, void *userdata)
+{
+ QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata);
+ return mdi->cbLeaveSpan(type, detail);
+}
+
+static int CbText(MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size, void *userdata)
+{
+ QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata);
+ return mdi->cbText(type, text, size);
+}
+
+static void CbDebugLog(const char *msg, void *userdata)
+{
+ Q_UNUSED(userdata)
+ qCDebug(lcMD) << msg;
+}
+
+// MD4C callback function wrappers
+// --------------------------------------------------------
+
+static Qt::Alignment MdAlignment(MD_ALIGN a, Qt::Alignment defaultAlignment = Qt::AlignLeft | Qt::AlignVCenter)
+{
+ switch (a) {
+ case MD_ALIGN_LEFT:
+ return Qt::AlignLeft | Qt::AlignVCenter;
+ case MD_ALIGN_CENTER:
+ return Qt::AlignHCenter | Qt::AlignVCenter;
+ case MD_ALIGN_RIGHT:
+ return Qt::AlignRight | Qt::AlignVCenter;
+ default: // including MD_ALIGN_DEFAULT
+ return defaultAlignment;
+ }
+}
+
+QTextMarkdownImporter::QTextMarkdownImporter(QTextMarkdownImporter::Features features)
+ : m_monoFont(QFontDatabase::systemFont(QFontDatabase::FixedFont))
+ , m_features(features)
+{
+}
+
+void QTextMarkdownImporter::import(QTextDocument *doc, const QString &markdown)
+{
+ MD_PARSER callbacks = {
+ 0, // abi_version
+ m_features,
+ &CbEnterBlock,
+ &CbLeaveBlock,
+ &CbEnterSpan,
+ &CbLeaveSpan,
+ &CbText,
+ &CbDebugLog,
+ nullptr // syntax
+ };
+ m_doc = doc;
+ m_cursor = new QTextCursor(doc);
+ doc->clear();
+ qCDebug(lcMD) << "default font" << doc->defaultFont() << "mono font" << m_monoFont;
+ QByteArray md = markdown.toUtf8();
+ md_parse(md.constData(), md.size(), &callbacks, this);
+ delete m_cursor;
+ m_cursor = nullptr;
+}
+
+int QTextMarkdownImporter::cbEnterBlock(MD_BLOCKTYPE type, void *det)
+{
+ m_blockType = type;
+ switch (type) {
+ case MD_BLOCK_P: {
+ QTextBlockFormat blockFmt;
+ int margin = m_doc->defaultFont().pointSize() / 2;
+ blockFmt.setTopMargin(margin);
+ blockFmt.setBottomMargin(margin);
+ m_cursor->insertBlock(blockFmt, QTextCharFormat());
+ } break;
+ case MD_BLOCK_CODE: {
+ QTextBlockFormat blockFmt;
+ QTextCharFormat charFmt;
+ charFmt.setFont(m_monoFont);
+ m_cursor->insertBlock(blockFmt, charFmt);
+ } break;
+ case MD_BLOCK_H: {
+ MD_BLOCK_H_DETAIL *detail = static_cast<MD_BLOCK_H_DETAIL *>(det);
+ QTextBlockFormat blockFmt;
+ QTextCharFormat charFmt;
+ int sizeAdjustment = 4 - detail->level; // H1 to H6: +3 to -2
+ charFmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment);
+ charFmt.setFontWeight(QFont::Bold);
+ blockFmt.setHeadingLevel(detail->level);
+ m_cursor->insertBlock(blockFmt, charFmt);
+ } break;
+ case MD_BLOCK_LI: {
+ MD_BLOCK_LI_DETAIL *detail = static_cast<MD_BLOCK_LI_DETAIL *>(det);
+ QTextBlockFormat bfmt = m_cursor->blockFormat();
+ bfmt.setMarker(detail->is_task ?
+ (detail->task_mark == ' ' ? QTextBlockFormat::Unchecked : QTextBlockFormat::Checked) :
+ QTextBlockFormat::NoMarker);
+ if (!m_emptyList) {
+ m_cursor->insertBlock(bfmt, QTextCharFormat());
+ m_listStack.top()->add(m_cursor->block());
+ }
+ m_cursor->setBlockFormat(bfmt);
+ m_emptyList = false; // Avoid insertBlock for the first item (because insertList already did that)
+ } break;
+ case MD_BLOCK_UL: {
+ MD_BLOCK_UL_DETAIL *detail = static_cast<MD_BLOCK_UL_DETAIL *>(det);
+ QTextListFormat fmt;
+ fmt.setIndent(m_listStack.count() + 1);
+ switch (detail->mark) {
+ case '*':
+ fmt.setStyle(QTextListFormat::ListCircle);
+ break;
+ case '+':
+ fmt.setStyle(QTextListFormat::ListSquare);
+ break;
+ default: // including '-'
+ fmt.setStyle(QTextListFormat::ListDisc);
+ break;
+ }
+ m_listStack.push(m_cursor->insertList(fmt));
+ m_emptyList = true;
+ } break;
+ case MD_BLOCK_OL: {
+ MD_BLOCK_OL_DETAIL *detail = static_cast<MD_BLOCK_OL_DETAIL *>(det);
+ QTextListFormat fmt;
+ fmt.setIndent(m_listStack.count() + 1);
+ fmt.setNumberSuffix(QChar::fromLatin1(detail->mark_delimiter));
+ fmt.setStyle(QTextListFormat::ListDecimal);
+ m_listStack.push(m_cursor->insertList(fmt));
+ m_emptyList = true;
+ } break;
+ case MD_BLOCK_TD: {
+ MD_BLOCK_TD_DETAIL *detail = static_cast<MD_BLOCK_TD_DETAIL *>(det);
+ ++m_tableCol;
+ // absolute movement (and storage of m_tableCol) shouldn't be necessary, but
+ // movePosition(QTextCursor::NextCell) doesn't work
+ QTextTableCell cell = m_currentTable->cellAt(m_tableRowCount - 1, m_tableCol);
+ if (!cell.isValid()) {
+ qWarning("malformed table in Markdown input");
+ return 1;
+ }
+ *m_cursor = cell.firstCursorPosition();
+ QTextBlockFormat blockFmt = m_cursor->blockFormat();
+ blockFmt.setAlignment(MdAlignment(detail->align));
+ m_cursor->setBlockFormat(blockFmt);
+ qCDebug(lcMD) << "TD; align" << detail->align << MdAlignment(detail->align) << "col" << m_tableCol;
+ } break;
+ case MD_BLOCK_TH: {
+ ++m_tableColumnCount;
+ ++m_tableCol;
+ if (m_currentTable->columns() < m_tableColumnCount)
+ m_currentTable->appendColumns(1);
+ auto cell = m_currentTable->cellAt(m_tableRowCount - 1, m_tableCol);
+ if (!cell.isValid()) {
+ qWarning("malformed table in Markdown input");
+ return 1;
+ }
+ auto fmt = cell.format();
+ fmt.setFontWeight(QFont::Bold);
+ cell.setFormat(fmt);
+ } break;
+ case MD_BLOCK_TR: {
+ ++m_tableRowCount;
+ m_nonEmptyTableCells.clear();
+ if (m_currentTable->rows() < m_tableRowCount)
+ m_currentTable->appendRows(1);
+ m_tableCol = -1;
+ qCDebug(lcMD) << "TR" << m_currentTable->rows();
+ } break;
+ case MD_BLOCK_TABLE:
+ m_tableColumnCount = 0;
+ m_tableRowCount = 0;
+ m_currentTable = m_cursor->insertTable(1, 1); // we don't know the dimensions yet
+ break;
+ case MD_BLOCK_HR: {
+ QTextBlockFormat blockFmt = m_cursor->blockFormat();
+ blockFmt.setProperty(QTextFormat::BlockTrailingHorizontalRulerWidth, 1);
+ m_cursor->insertBlock(blockFmt, QTextCharFormat());
+ } break;
+ default:
+ break; // nothing to do for now
+ }
+ return 0; // no error
+}
+
+int QTextMarkdownImporter::cbLeaveBlock(MD_BLOCKTYPE type, void *detail)
+{
+ Q_UNUSED(detail)
+ switch (type) {
+ case MD_BLOCK_UL:
+ case MD_BLOCK_OL:
+ m_listStack.pop();
+ break;
+ case MD_BLOCK_TR: {
+ // https://github.com/mity/md4c/issues/29
+ // MD4C doesn't tell us explicitly which cells are merged, so merge empty cells
+ // with previous non-empty ones
+ int mergeEnd = -1;
+ int mergeBegin = -1;
+ for (int col = m_tableCol; col >= 0; --col) {
+ if (m_nonEmptyTableCells.contains(col)) {
+ if (mergeEnd >= 0 && mergeBegin >= 0) {
+ qCDebug(lcMD) << "merging cells" << mergeBegin << "to" << mergeEnd << "inclusive, on row" << m_currentTable->rows() - 1;
+ m_currentTable->mergeCells(m_currentTable->rows() - 1, mergeBegin - 1, 1, mergeEnd - mergeBegin + 2);
+ }
+ mergeEnd = -1;
+ mergeBegin = -1;
+ } else {
+ if (mergeEnd < 0)
+ mergeEnd = col;
+ else
+ mergeBegin = col;
+ }
+ }
+ } break;
+ case MD_BLOCK_QUOTE: {
+ QTextBlockFormat blockFmt = m_cursor->blockFormat();
+ blockFmt.setIndent(1);
+ m_cursor->setBlockFormat(blockFmt);
+ } break;
+ case MD_BLOCK_TABLE:
+ qCDebug(lcMD) << "table ended with" << m_currentTable->columns() << "cols and" << m_currentTable->rows() << "rows";
+ m_currentTable = nullptr;
+ m_cursor->movePosition(QTextCursor::End);
+ break;
+ default:
+ break;
+ }
+ return 0; // no error
+}
+
+int QTextMarkdownImporter::cbEnterSpan(MD_SPANTYPE type, void *det)
+{
+ QTextCharFormat charFmt;
+ switch (type) {
+ case MD_SPAN_EM:
+ charFmt.setFontItalic(true);
+ break;
+ case MD_SPAN_STRONG:
+ charFmt.setFontWeight(QFont::Bold);
+ break;
+ case MD_SPAN_A: {
+ MD_SPAN_A_DETAIL *detail = static_cast<MD_SPAN_A_DETAIL *>(det);
+ QString url = QString::fromLatin1(detail->href.text, detail->href.size);
+ QString title = QString::fromLatin1(detail->title.text, detail->title.size);
+ charFmt.setAnchorHref(url);
+ charFmt.setAnchorName(title);
+ charFmt.setForeground(m_palette.link());
+ qCDebug(lcMD) << "anchor" << url << title;
+ } break;
+ case MD_SPAN_IMG: {
+ m_imageSpan = true;
+ MD_SPAN_IMG_DETAIL *detail = static_cast<MD_SPAN_IMG_DETAIL *>(det);
+ QString src = QString::fromUtf8(detail->src.text, detail->src.size);
+ QString title = QString::fromUtf8(detail->title.text, detail->title.size);
+ QTextImageFormat img;
+ img.setName(src);
+ qCDebug(lcMD) << "image" << src << "title" << title << "relative to" << m_doc->baseUrl();
+ m_cursor->insertImage(img);
+ break;
+ }
+ case MD_SPAN_CODE:
+ charFmt.setFont(m_monoFont);
+ break;
+ case MD_SPAN_DEL:
+ charFmt.setFontStrikeOut(true);
+ break;
+ }
+ m_spanFormatStack.push(charFmt);
+ m_cursor->setCharFormat(charFmt);
+ return 0; // no error
+}
+
+int QTextMarkdownImporter::cbLeaveSpan(MD_SPANTYPE type, void *detail)
+{
+ Q_UNUSED(detail)
+ QTextCharFormat charFmt;
+ if (!m_spanFormatStack.isEmpty()) {
+ m_spanFormatStack.pop();
+ if (!m_spanFormatStack.isEmpty())
+ charFmt = m_spanFormatStack.top();
+ }
+ m_cursor->setCharFormat(charFmt);
+ if (type == MD_SPAN_IMG)
+ m_imageSpan = false;
+ return 0; // no error
+}
+
+int QTextMarkdownImporter::cbText(MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size)
+{
+ if (m_imageSpan)
+ return 0; // it's the alt-text
+ static const QRegularExpression openingBracket(QStringLiteral("<[a-zA-Z]"));
+ static const QRegularExpression closingBracket(QStringLiteral("(/>|</)"));
+ QString s = QString::fromUtf8(text, size);
+
+ switch (type) {
+ case MD_TEXT_NORMAL:
+ if (m_htmlTagDepth) {
+ m_htmlAccumulator += s;
+ s = QString();
+ }
+ break;
+ case MD_TEXT_NULLCHAR:
+ s = QString(QChar(0xFFFD)); // CommonMark-required replacement for null
+ break;
+ case MD_TEXT_BR:
+ s = QLatin1String("\n");
+ break;
+ case MD_TEXT_SOFTBR:
+ s = QLatin1String(" ");
+ break;
+ case MD_TEXT_CODE:
+ // We'll see MD_SPAN_CODE too, which will set the char format, and that's enough.
+ break;
+ case MD_TEXT_ENTITY:
+ m_cursor->insertHtml(s);
+ s = QString();
+ break;
+ case MD_TEXT_HTML:
+ // count how many tags are opened and how many are closed
+ {
+ int startIdx = 0;
+ while ((startIdx = s.indexOf(openingBracket, startIdx)) >= 0) {
+ ++m_htmlTagDepth;
+ startIdx += 2;
+ }
+ startIdx = 0;
+ while ((startIdx = s.indexOf(closingBracket, startIdx)) >= 0) {
+ --m_htmlTagDepth;
+ startIdx += 2;
+ }
+ }
+ m_htmlAccumulator += s;
+ s = QString();
+ if (!m_htmlTagDepth) { // all open tags are now closed
+ qCDebug(lcMD) << "HTML" << m_htmlAccumulator;
+ m_cursor->insertHtml(m_htmlAccumulator);
+ if (m_spanFormatStack.isEmpty())
+ m_cursor->setCharFormat(QTextCharFormat());
+ else
+ m_cursor->setCharFormat(m_spanFormatStack.top());
+ m_htmlAccumulator = QString();
+ }
+ break;
+ }
+
+ switch (m_blockType) {
+ case MD_BLOCK_TD:
+ m_nonEmptyTableCells.append(m_tableCol);
+ break;
+ default:
+ break;
+ }
+
+ if (!s.isEmpty())
+ m_cursor->insertText(s);
+ return 0; // no error
+}
+
+QT_END_NAMESPACE
diff --git a/src/gui/text/qtextmarkdownimporter_p.h b/src/gui/text/qtextmarkdownimporter_p.h
new file mode 100644
index 0000000000..b73e744434
--- /dev/null
+++ b/src/gui/text/qtextmarkdownimporter_p.h
@@ -0,0 +1,127 @@
+/****************************************************************************
+**
+** 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 QTEXTMARKDOWNIMPORTER_H
+#define QTEXTMARKDOWNIMPORTER_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/qfont.h>
+#include <QtGui/qtguiglobal.h>
+#include <QtGui/qpalette.h>
+#include <QtGui/qtextlist.h>
+#include <QtCore/qstack.h>
+
+#include "../../3rdparty/md4c/md4c.h"
+
+QT_BEGIN_NAMESPACE
+
+class QTextCursor;
+class QTextDocument;
+class QTextTable;
+
+class Q_GUI_EXPORT QTextMarkdownImporter
+{
+public:
+ enum Feature {
+ FeatureCollapseWhitespace = MD_FLAG_COLLAPSEWHITESPACE,
+ FeaturePermissiveATXHeaders = MD_FLAG_PERMISSIVEATXHEADERS,
+ FeaturePermissiveURLAutoLinks = MD_FLAG_PERMISSIVEURLAUTOLINKS,
+ FeaturePermissiveMailAutoLinks = MD_FLAG_PERMISSIVEEMAILAUTOLINKS,
+ FeatureNoIndentedCodeBlocks = MD_FLAG_NOINDENTEDCODEBLOCKS,
+ FeatureNoHTMLBlocks = MD_FLAG_NOHTMLBLOCKS,
+ FeatureNoHTMLSpans = MD_FLAG_NOHTMLSPANS,
+ FeatureTables = MD_FLAG_TABLES,
+ FeatureStrikeThrough = MD_FLAG_STRIKETHROUGH,
+ FeaturePermissiveWWWAutoLinks = MD_FLAG_PERMISSIVEWWWAUTOLINKS,
+ FeatureTasklists = MD_FLAG_TASKLISTS,
+ // composite flags
+ FeaturePermissiveAutoLinks = MD_FLAG_PERMISSIVEAUTOLINKS,
+ FeatureNoHTML = MD_FLAG_NOHTML,
+ DialectCommonMark = MD_DIALECT_COMMONMARK,
+ DialectGitHub = MD_DIALECT_GITHUB
+ };
+ Q_DECLARE_FLAGS(Features, Feature)
+
+ QTextMarkdownImporter(Features features);
+
+ void import(QTextDocument *doc, const QString &markdown);
+
+public:
+ // MD4C callbacks
+ int cbEnterBlock(MD_BLOCKTYPE type, void* detail);
+ int cbLeaveBlock(MD_BLOCKTYPE type, void* detail);
+ int cbEnterSpan(MD_SPANTYPE type, void* detail);
+ int cbLeaveSpan(MD_SPANTYPE type, void* detail);
+ int cbText(MD_TEXTTYPE type, const MD_CHAR* text, MD_SIZE size);
+
+private:
+ QTextDocument *m_doc = nullptr;
+ QTextCursor *m_cursor = nullptr;
+ QTextTable *m_currentTable = nullptr; // because m_cursor->currentTable() doesn't work
+ QString m_htmlAccumulator;
+ QVector<int> m_nonEmptyTableCells; // in the current row
+ QStack<QTextList *> m_listStack;
+ QStack<QTextCharFormat> m_spanFormatStack;
+ QFont m_monoFont;
+ QPalette m_palette;
+ int m_htmlTagDepth = 0;
+ int m_tableColumnCount = 0;
+ int m_tableRowCount = 0;
+ int m_tableCol = -1; // because relative cell movements (e.g. m_cursor->movePosition(QTextCursor::NextCell)) don't work
+ Features m_features;
+ MD_BLOCKTYPE m_blockType = MD_BLOCK_DOC;
+ bool m_emptyList = false; // true when the last thing we did was insertList
+ bool m_imageSpan = false;
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(QTextMarkdownImporter::Features)
+
+QT_END_NAMESPACE
+
+#endif // QTEXTMARKDOWNIMPORTER_H
diff --git a/src/gui/text/text.pri b/src/gui/text/text.pri
index abe20abe02..b35a231747 100644
--- a/src/gui/text/text.pri
+++ b/src/gui/text/text.pri
@@ -97,6 +97,18 @@ qtConfig(textodfwriter) {
text/qzip.cpp
}
+qtConfig(textmarkdownreader) {
+ qtConfig(system-textmarkdownreader) {
+ QMAKE_USE += libmd4c
+ } else {
+ include($$PWD/../../3rdparty/md4c.pri)
+ }
+ HEADERS += \
+ text/qtextmarkdownimporter_p.h
+ SOURCES += \
+ text/qtextmarkdownimporter.cpp
+}
+
qtConfig(cssparser) {
HEADERS += \
text/qcssparser_p.h
diff --git a/src/widgets/widgets/qtextedit.cpp b/src/widgets/widgets/qtextedit.cpp
index 920133d493..4a06f58b4a 100644
--- a/src/widgets/widgets/qtextedit.cpp
+++ b/src/widgets/widgets/qtextedit.cpp
@@ -1,6 +1,6 @@
/****************************************************************************
**
-** Copyright (C) 2016 The Qt Company Ltd.
+** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWidgets module of the Qt Toolkit.
@@ -1202,6 +1202,13 @@ QString QTextEdit::toHtml() const
}
#endif
+#if QT_CONFIG(textmarkdownreader)
+void QTextEdit::setMarkdown(const QString &text)
+{
+ Q_D(const QTextEdit);
+ d->control->setMarkdown(text);
+}
+#endif
/*! \reimp
*/
diff --git a/src/widgets/widgets/qtextedit.h b/src/widgets/widgets/qtextedit.h
index 3aa23aaace..f20bd936c4 100644
--- a/src/widgets/widgets/qtextedit.h
+++ b/src/widgets/widgets/qtextedit.h
@@ -1,6 +1,6 @@
/****************************************************************************
**
-** Copyright (C) 2016 The Qt Company Ltd.
+** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWidgets module of the Qt Toolkit.
@@ -238,6 +238,9 @@ public Q_SLOTS:
#ifndef QT_NO_TEXTHTMLPARSER
void setHtml(const QString &text);
#endif
+#if QT_CONFIG(textmarkdownreader)
+ void setMarkdown(const QString &text);
+#endif
void setText(const QString &text);
#ifndef QT_NO_CLIPBOARD
diff --git a/src/widgets/widgets/qwidgettextcontrol.cpp b/src/widgets/widgets/qwidgettextcontrol.cpp
index 711c4bfd2a..5744d43cbf 100644
--- a/src/widgets/widgets/qwidgettextcontrol.cpp
+++ b/src/widgets/widgets/qwidgettextcontrol.cpp
@@ -1,6 +1,6 @@
/****************************************************************************
**
-** Copyright (C) 2016 The Qt Company Ltd.
+** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWidgets module of the Qt Toolkit.
@@ -491,6 +491,11 @@ void QWidgetTextControlPrivate::setContent(Qt::TextFormat format, const QString
formatCursor.select(QTextCursor::Document);
formatCursor.setCharFormat(charFormatForInsertion);
formatCursor.endEditBlock();
+#if QT_CONFIG(textmarkdownreader)
+ } else if (format == Qt::MarkdownText) {
+ doc->setMarkdown(text);
+ doc->setUndoRedoEnabled(false);
+#endif
} else {
#ifndef QT_NO_TEXTHTMLPARSER
doc->setHtml(text);
@@ -1194,6 +1199,14 @@ void QWidgetTextControl::setPlainText(const QString &text)
d->setContent(Qt::PlainText, text);
}
+#if QT_CONFIG(textmarkdownreader)
+void QWidgetTextControl::setMarkdown(const QString &text)
+{
+ Q_D(QWidgetTextControl);
+ d->setContent(Qt::MarkdownText, text);
+}
+#endif
+
void QWidgetTextControl::setHtml(const QString &text)
{
Q_D(QWidgetTextControl);
diff --git a/src/widgets/widgets/qwidgettextcontrol_p.h b/src/widgets/widgets/qwidgettextcontrol_p.h
index 9c80d53728..4c9e47dfc9 100644
--- a/src/widgets/widgets/qwidgettextcontrol_p.h
+++ b/src/widgets/widgets/qwidgettextcontrol_p.h
@@ -1,6 +1,6 @@
/****************************************************************************
**
-** Copyright (C) 2016 The Qt Company Ltd.
+** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWidgets module of the Qt Toolkit.
@@ -194,6 +194,9 @@ public:
public Q_SLOTS:
void setPlainText(const QString &text);
+#if QT_CONFIG(textmarkdownreader)
+ void setMarkdown(const QString &text);
+#endif
void setHtml(const QString &text);
#ifndef QT_NO_CLIPBOARD
diff --git a/src/widgets/widgets/qwidgettextcontrol_p_p.h b/src/widgets/widgets/qwidgettextcontrol_p_p.h
index 6a1ee564cd..6ccdfafe2b 100644
--- a/src/widgets/widgets/qwidgettextcontrol_p_p.h
+++ b/src/widgets/widgets/qwidgettextcontrol_p_p.h
@@ -1,6 +1,6 @@
/****************************************************************************
**
-** Copyright (C) 2016 The Qt Company Ltd.
+** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWidgets module of the Qt Toolkit.