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>
This commit is contained in:
Shawn Rutledge 2017-12-19 15:25:55 +01:00
parent 9ec564b0bf
commit 23c2da3cc2
18 changed files with 1124 additions and 11 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(text);
d->control->setMarkdown(markdown);
}
#endif
#if QT_CONFIG(textmarkdownwriter)
QString QTextEdit::toMarkdown(QTextDocument::MarkdownFeatures features) const
{
Q_D(const QTextEdit);
return d->control->toMarkdown(features);
}
#endif

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
[rewriteDocument]
winrt # QTBUG-54623

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
TEMPLATE = app
TARGET = html2md
INCLUDEPATH += .
#QT += gui-private
SOURCES += html2md.cpp