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:
parent
860bf13dbd
commit
65314b6ce8
@ -1198,7 +1198,8 @@ public:
|
||||
enum TextFormat {
|
||||
PlainText,
|
||||
RichText,
|
||||
AutoText
|
||||
AutoText,
|
||||
MarkdownText
|
||||
};
|
||||
|
||||
enum AspectRatioMode {
|
||||
|
@ -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",
|
||||
{
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
435
src/gui/text/qtextmarkdownimporter.cpp
Normal file
435
src/gui/text/qtextmarkdownimporter.cpp
Normal 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
|
127
src/gui/text/qtextmarkdownimporter_p.h
Normal file
127
src/gui/text/qtextmarkdownimporter_p.h
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user