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>
This commit is contained in:
Shawn Rutledge 2017-12-18 08:55:18 +01:00
parent 860bf13dbd
commit 65314b6ce8
13 changed files with 696 additions and 8 deletions

View File

@ -1198,7 +1198,8 @@ public:
enum TextFormat {
PlainText,
RichText,
AutoText
AutoText,
MarkdownText
};
enum AspectRatioMode {

View File

@ -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",
{

View File

@ -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>
@ -3285,6 +3288,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.
*/

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
*/

View File

@ -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.
@ -237,6 +237,9 @@ public Q_SLOTS:
void setPlainText(const QString &text);
#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);

View File

@ -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);

View File

@ -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

View File

@ -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.