summaryrefslogtreecommitdiffstats
path: root/src/gui/text
diff options
context:
space:
mode:
authorShawn Rutledge <shawn.rutledge@qt.io>2017-12-19 15:25:55 +0100
committerShawn Rutledge <shawn.rutledge@qt.io>2019-05-01 14:31:27 +0000
commit23c2da3cc23a2e04a0b3b3c8ad7fa9cc6126ff23 (patch)
tree849770044988425cf290b8599b44440653bcddd0 /src/gui/text
parent9ec564b0bfc94c2d33f02b24ca081b64ff9ebb9b (diff)
Add QTextMarkdownWriter, QTextEdit::markdown property etc.
A QTextDocument can now be written out in Markdown format. - Add the QTextMarkdownWriter as a private class for now - Add QTextDocument::toMarkdown() - QTextDocumentWriter uses QTextMarkdownWriter if setFormat("markdown") is called or if the file suffix is .md or .mkd - Add QTextEdit::toMarkdown() and the markdown property [ChangeLog][QtGui][Text] Markdown (CommonMark or GitHub dialect) is now a supported format for reading into and writing from QTextDocument. Change-Id: I663a77017fac7ae1b3f9a400f5cd357bb40750af Reviewed-by: Gatis Paeglis <gatis.paeglis@qt.io>
Diffstat (limited to 'src/gui/text')
-rw-r--r--src/gui/text/qtextdocument.cpp34
-rw-r--r--src/gui/text/qtextdocument.h8
-rw-r--r--src/gui/text/qtextdocumentwriter.cpp19
-rw-r--r--src/gui/text/qtextmarkdownwriter.cpp363
-rw-r--r--src/gui/text/qtextmarkdownwriter_p.h78
-rw-r--r--src/gui/text/text.pri7
6 files changed, 506 insertions, 3 deletions
diff --git a/src/gui/text/qtextdocument.cpp b/src/gui/text/qtextdocument.cpp
index 87c8f1ba8a..0a59bfb838 100644
--- a/src/gui/text/qtextdocument.cpp
+++ b/src/gui/text/qtextdocument.cpp
@@ -73,6 +73,9 @@
#if QT_CONFIG(textmarkdownreader)
#include <private/qtextmarkdownimporter_p.h>
#endif
+#if QT_CONFIG(textmarkdownwriter)
+#include <private/qtextmarkdownwriter_p.h>
+#endif
#include <limits.h>
@@ -3289,6 +3292,22 @@ QString QTextDocument::toHtml(const QByteArray &encoding) const
#endif // QT_NO_TEXTHTMLPARSER
/*!
+ Returns a string containing a Markdown representation of the document,
+ or an empty string if writing fails for any reason.
+*/
+#if QT_CONFIG(textmarkdownwriter)
+QString QTextDocument::toMarkdown(QTextDocument::MarkdownFeatures features) const
+{
+ QString ret;
+ QTextStream s(&ret);
+ QTextMarkdownWriter w(s, features);
+ if (w.writeAll(*this))
+ return ret;
+ return QString();
+}
+#endif
+
+/*!
Replaces the entire contents of the document with the given
Markdown-formatted text in the \a markdown string, with the given
\a features supported. By default, all supported GitHub-style
@@ -3301,8 +3320,19 @@ QString QTextDocument::toHtml(const QByteArray &encoding) const
Parsing of HTML included in the \a markdown string is handled in the same
way as in \l setHtml; however, Markdown formatting inside HTML blocks is
- not supported. The \c MarkdownNoHTML feature flag can be set to disable
- HTML parsing.
+ not supported.
+
+ Some features of the parser can be enabled or disabled via the \a features
+ argument:
+
+ \value MarkdownNoHTML
+ Any HTML tags in the Markdown text will be discarded
+ \value MarkdownDialectCommonMark
+ The parser supports only the features standardized by CommonMark
+ \value MarkdownDialectGitHub
+ The parser supports the GitHub dialect
+
+ The default is \c MarkdownDialectGitHub.
The undo/redo history is reset when this function is called.
*/
diff --git a/src/gui/text/qtextdocument.h b/src/gui/text/qtextdocument.h
index ade67999ad..31c06976a5 100644
--- a/src/gui/text/qtextdocument.h
+++ b/src/gui/text/qtextdocument.h
@@ -151,7 +151,7 @@ public:
void setHtml(const QString &html);
#endif
-#if QT_CONFIG(textmarkdownreader)
+#if QT_CONFIG(textmarkdownwriter) || QT_CONFIG(textmarkdownreader)
// Must be in sync with QTextMarkdownImporter::Features, should be in sync with #define MD_FLAG_* in md4c
enum MarkdownFeature {
MarkdownNoHTML = 0x0020 | 0x0040,
@@ -160,7 +160,13 @@ public:
};
Q_DECLARE_FLAGS(MarkdownFeatures, MarkdownFeature)
Q_FLAG(MarkdownFeatures)
+#endif
+#if QT_CONFIG(textmarkdownwriter)
+ QString toMarkdown(MarkdownFeatures features = MarkdownDialectGitHub) const;
+#endif
+
+#if QT_CONFIG(textmarkdownreader)
void setMarkdown(const QString &markdown, MarkdownFeatures features = MarkdownDialectGitHub);
#endif
diff --git a/src/gui/text/qtextdocumentwriter.cpp b/src/gui/text/qtextdocumentwriter.cpp
index 42e623153a..c82ff873cd 100644
--- a/src/gui/text/qtextdocumentwriter.cpp
+++ b/src/gui/text/qtextdocumentwriter.cpp
@@ -51,6 +51,9 @@
#include "qtextdocumentfragment_p.h"
#include "qtextodfwriter_p.h"
+#if QT_CONFIG(textmarkdownwriter)
+#include "qtextmarkdownwriter_p.h"
+#endif
#include <algorithm>
@@ -267,6 +270,18 @@ bool QTextDocumentWriter::write(const QTextDocument *document)
}
#endif // QT_NO_TEXTODFWRITER
+#if QT_CONFIG(textmarkdownwriter)
+ if (format == "md" || format == "mkd" || format == "markdown") {
+ if (!d->device->isWritable() && !d->device->open(QIODevice::WriteOnly)) {
+ qWarning("QTextDocumentWriter::write: the device can not be opened for writing");
+ return false;
+ }
+ QTextStream s(d->device);
+ QTextMarkdownWriter writer(s, QTextDocument::MarkdownDialectGitHub);
+ return writer.writeAll(*document);
+ }
+#endif // textmarkdownwriter
+
#ifndef QT_NO_TEXTHTMLPARSER
if (format == "html" || format == "htm") {
if (!d->device->isWritable() && ! d->device->open(QIODevice::WriteOnly)) {
@@ -348,6 +363,7 @@ QTextCodec *QTextDocumentWriter::codec() const
\header \li Format \li Description
\row \li plaintext \li Plain text
\row \li HTML \li HyperText Markup Language
+ \row \li markdown \li Markdown (CommonMark or GitHub dialects)
\row \li ODF \li OpenDocument Format
\endtable
@@ -364,6 +380,9 @@ QList<QByteArray> QTextDocumentWriter::supportedDocumentFormats()
#ifndef QT_NO_TEXTODFWRITER
answer << "ODF";
#endif // QT_NO_TEXTODFWRITER
+#if QT_CONFIG(textmarkdownwriter)
+ answer << "markdown";
+#endif
std::sort(answer.begin(), answer.end());
return answer;
diff --git a/src/gui/text/qtextmarkdownwriter.cpp b/src/gui/text/qtextmarkdownwriter.cpp
new file mode 100644
index 0000000000..c91248757a
--- /dev/null
+++ b/src/gui/text/qtextmarkdownwriter.cpp
@@ -0,0 +1,363 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtGui module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "qtextmarkdownwriter_p.h"
+#include "qtextdocumentlayout_p.h"
+#include "qfontinfo.h"
+#include "qfontmetrics.h"
+#include "qtextdocument_p.h"
+#include "qtextlist.h"
+#include "qtexttable.h"
+#include "qtextcursor.h"
+#include "qtextimagehandler_p.h"
+
+QT_BEGIN_NAMESPACE
+
+static const QChar Space = QLatin1Char(' ');
+static const QChar Newline = QLatin1Char('\n');
+static const QChar Backtick = QLatin1Char('`');
+
+QTextMarkdownWriter::QTextMarkdownWriter(QTextStream &stream, QTextDocument::MarkdownFeatures features)
+ : m_stream(stream), m_features(features)
+{
+}
+
+bool QTextMarkdownWriter::writeAll(const QTextDocument &document)
+{
+ writeFrame(document.rootFrame());
+ return true;
+}
+
+void QTextMarkdownWriter::writeFrame(const QTextFrame *frame)
+{
+ Q_ASSERT(frame);
+ const QTextTable *table = qobject_cast<const QTextTable*> (frame);
+ QTextFrame::iterator iterator = frame->begin();
+ QTextFrame *child = 0;
+ int tableRow = -1;
+ bool lastWasList = false;
+ QVector<int> tableColumnWidths;
+ if (table) {
+ tableColumnWidths.resize(table->columns());
+ for (int col = 0; col < table->columns(); ++col) {
+ for (int row = 0; row < table->rows(); ++ row) {
+ QTextTableCell cell = table->cellAt(row, col);
+ int cellTextLen = 0;
+ auto it = cell.begin();
+ while (it != cell.end()) {
+ QTextBlock block = it.currentBlock();
+ if (block.isValid())
+ cellTextLen += block.text().length();
+ ++it;
+ }
+ if (cell.columnSpan() == 1 && tableColumnWidths[col] < cellTextLen)
+ tableColumnWidths[col] = cellTextLen;
+ }
+ }
+ }
+ while (!iterator.atEnd()) {
+ if (iterator.currentFrame() && child != iterator.currentFrame())
+ writeFrame(iterator.currentFrame());
+ else { // no frame, it's a block
+ QTextBlock block = iterator.currentBlock();
+ if (table) {
+ QTextTableCell cell = table->cellAt(block.position());
+ if (tableRow < cell.row()) {
+ if (tableRow == 0) {
+ m_stream << Newline;
+ for (int col = 0; col < tableColumnWidths.length(); ++col)
+ m_stream << '|' << QString(tableColumnWidths[col], QLatin1Char('-'));
+ m_stream << '|';
+ }
+ m_stream << Newline << "|";
+ tableRow = cell.row();
+ }
+ } else if (!block.textList()) {
+ if (lastWasList)
+ m_stream << Newline;
+ }
+ int endingCol = writeBlock(block, !table, table && tableRow == 0);
+ if (table) {
+ QTextTableCell cell = table->cellAt(block.position());
+ int paddingLen = -endingCol;
+ int spanEndCol = cell.column() + cell.columnSpan();
+ for (int col = cell.column(); col < spanEndCol; ++col)
+ paddingLen += tableColumnWidths[col];
+ if (paddingLen > 0)
+ m_stream << QString(paddingLen, Space);
+ for (int col = cell.column(); col < spanEndCol; ++col)
+ m_stream << "|";
+ } else if (block.textList()) {
+ m_stream << Newline;
+ } else if (endingCol > 0) {
+ m_stream << Newline << Newline;
+ }
+ lastWasList = block.textList();
+ }
+ child = iterator.currentFrame();
+ ++iterator;
+ }
+ if (table)
+ m_stream << Newline << Newline;
+}
+
+static int nearestWordWrapIndex(const QString &s, int before)
+{
+ before = qMin(before, s.length());
+ for (int i = before - 1; i >= 0; --i) {
+ if (s.at(i).isSpace())
+ return i;
+ }
+ return -1;
+}
+
+static int adjacentBackticksCount(const QString &s)
+{
+ int start = -1, len = s.length();
+ int ret = 0;
+ for (int i = 0; i < len; ++i) {
+ if (s.at(i) == Backtick) {
+ if (start < 0)
+ start = i;
+ } else if (start >= 0) {
+ ret = qMax(ret, i - start);
+ start = -1;
+ }
+ }
+ if (s.at(len - 1) == Backtick)
+ ret = qMax(ret, len - start);
+ return ret;
+}
+
+static void maybeEscapeFirstChar(QString &s)
+{
+ QString sTrimmed = s.trimmed();
+ if (sTrimmed.isEmpty())
+ return;
+ char firstChar = sTrimmed.at(0).toLatin1();
+ if (firstChar == '*' || firstChar == '+' || firstChar == '-') {
+ int i = s.indexOf(QLatin1Char(firstChar));
+ s.insert(i, QLatin1Char('\\'));
+ }
+}
+
+int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ignoreFormat)
+{
+ int ColumnLimit = 80;
+ int wrapIndent = 0;
+ if (block.textList()) { // it's a list-item
+ auto fmt = block.textList()->format();
+ const int listLevel = fmt.indent();
+ const int number = block.textList()->itemNumber(block) + 1;
+ QByteArray bullet = " ";
+ bool numeric = false;
+ switch (fmt.style()) {
+ case QTextListFormat::ListDisc: bullet = "-"; break;
+ case QTextListFormat::ListCircle: bullet = "*"; break;
+ case QTextListFormat::ListSquare: bullet = "+"; break;
+ case QTextListFormat::ListStyleUndefined: break;
+ case QTextListFormat::ListDecimal:
+ case QTextListFormat::ListLowerAlpha:
+ case QTextListFormat::ListUpperAlpha:
+ case QTextListFormat::ListLowerRoman:
+ case QTextListFormat::ListUpperRoman:
+ numeric = true;
+ break;
+ }
+ switch (block.blockFormat().marker()) {
+ case QTextBlockFormat::Checked:
+ bullet += " [x]";
+ break;
+ case QTextBlockFormat::Unchecked:
+ bullet += " [ ]";
+ break;
+ default:
+ break;
+ }
+ QString prefix((listLevel - 1) * (numeric ? 4 : 2), Space);
+ if (numeric)
+ prefix += QString::number(number) + fmt.numberSuffix() + Space;
+ else
+ prefix += QLatin1String(bullet) + Space;
+ m_stream << prefix;
+ wrapIndent = prefix.length();
+ }
+
+ if (block.blockFormat().headingLevel())
+ m_stream << QByteArray(block.blockFormat().headingLevel(), '#') << ' ';
+
+ QString wrapIndentString(wrapIndent, Space);
+ // It would be convenient if QTextStream had a lineCharPos() accessor,
+ // to keep track of how many characters (not bytes) have been written on the current line,
+ // but it doesn't. So we have to keep track with this col variable.
+ int col = wrapIndent;
+ bool mono = false;
+ bool startsOrEndsWithBacktick = false;
+ bool bold = false;
+ bool italic = false;
+ bool underline = false;
+ bool strikeOut = false;
+ QString backticks(Backtick);
+ for (QTextBlock::Iterator frag = block.begin(); !frag.atEnd(); ++frag) {
+ QString fragmentText = frag.fragment().text();
+ while (fragmentText.endsWith(QLatin1Char('\n')))
+ fragmentText.chop(1);
+ startsOrEndsWithBacktick |= fragmentText.startsWith(Backtick) || fragmentText.endsWith(Backtick);
+ QTextCharFormat fmt = frag.fragment().charFormat();
+ if (fmt.isImageFormat()) {
+ QTextImageFormat ifmt = fmt.toImageFormat();
+ QString s = QLatin1String("![image](") + ifmt.name() + QLatin1Char(')');
+ if (wrap && col + s.length() > ColumnLimit) {
+ m_stream << Newline << wrapIndentString;
+ col = wrapIndent;
+ }
+ m_stream << s;
+ col += s.length();
+ } else if (fmt.hasProperty(QTextFormat::AnchorHref)) {
+ QString s = QLatin1Char('[') + fragmentText + QLatin1String("](") +
+ fmt.property(QTextFormat::AnchorHref).toString() + QLatin1Char(')');
+ if (wrap && col + s.length() > ColumnLimit) {
+ m_stream << Newline << wrapIndentString;
+ col = wrapIndent;
+ }
+ m_stream << s;
+ col += s.length();
+ } else {
+ QFontInfo fontInfo(fmt.font());
+ bool monoFrag = fontInfo.fixedPitch();
+ QString markers;
+ if (!ignoreFormat) {
+ if (monoFrag != mono) {
+ if (monoFrag)
+ backticks = QString::fromLatin1(QByteArray(adjacentBackticksCount(fragmentText) + 1, '`'));
+ markers += backticks;
+ if (startsOrEndsWithBacktick)
+ markers += Space;
+ mono = monoFrag;
+ }
+ if (!block.blockFormat().headingLevel() && !mono) {
+ if (fmt.font().bold() != bold) {
+ markers += QLatin1String("**");
+ bold = fmt.font().bold();
+ }
+ if (fmt.font().italic() != italic) {
+ markers += QLatin1Char('*');
+ italic = fmt.font().italic();
+ }
+ if (fmt.font().strikeOut() != strikeOut) {
+ markers += QLatin1String("~~");
+ strikeOut = fmt.font().strikeOut();
+ }
+ if (fmt.font().underline() != underline) {
+ // Markdown doesn't support underline, but the parser will treat a single underline
+ // the same as a single asterisk, and the marked fragment will be rendered in italics.
+ // That will have to do.
+ markers += QLatin1Char('_');
+ underline = fmt.font().underline();
+ }
+ }
+ }
+ if (wrap && col + markers.length() * 2 + fragmentText.length() > ColumnLimit) {
+ int i = 0;
+ int fragLen = fragmentText.length();
+ bool breakingLine = false;
+ while (i < fragLen) {
+ int j = i + ColumnLimit - col;
+ if (j < fragLen) {
+ int wi = nearestWordWrapIndex(fragmentText, j);
+ if (wi < 0) {
+ j = fragLen;
+ } else {
+ j = wi;
+ breakingLine = true;
+ }
+ } else {
+ j = fragLen;
+ breakingLine = false;
+ }
+ QString subfrag = fragmentText.mid(i, j - i);
+ if (!i) {
+ m_stream << markers;
+ col += markers.length();
+ }
+ if (col == wrapIndent)
+ maybeEscapeFirstChar(subfrag);
+ m_stream << subfrag;
+ if (breakingLine) {
+ m_stream << Newline << wrapIndentString;
+ col = wrapIndent;
+ } else {
+ col += subfrag.length();
+ }
+ i = j + 1;
+ }
+ } else {
+ m_stream << markers << fragmentText;
+ col += markers.length() + fragmentText.length();
+ }
+ }
+ }
+ if (mono) {
+ if (startsOrEndsWithBacktick) {
+ m_stream << Space;
+ col += 1;
+ }
+ m_stream << backticks;
+ col += backticks.size();
+ }
+ if (bold) {
+ m_stream << "**";
+ col += 2;
+ }
+ if (italic) {
+ m_stream << "*";
+ col += 1;
+ }
+ if (underline) {
+ m_stream << "_";
+ col += 1;
+ }
+ if (strikeOut) {
+ m_stream << "~~";
+ col += 2;
+ }
+ return col;
+}
+
+QT_END_NAMESPACE
diff --git a/src/gui/text/qtextmarkdownwriter_p.h b/src/gui/text/qtextmarkdownwriter_p.h
new file mode 100644
index 0000000000..9845355259
--- /dev/null
+++ b/src/gui/text/qtextmarkdownwriter_p.h
@@ -0,0 +1,78 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtGui module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QTEXTMARKDOWNWRITER_P_H
+#define QTEXTMARKDOWNWRITER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtGui/private/qtguiglobal_p.h>
+#include <QtCore/QTextStream>
+
+#include "qtextdocument_p.h"
+#include "qtextdocumentwriter.h"
+
+QT_BEGIN_NAMESPACE
+
+class Q_GUI_EXPORT QTextMarkdownWriter
+{
+public:
+ QTextMarkdownWriter(QTextStream &stream, QTextDocument::MarkdownFeatures features);
+ bool writeAll(const QTextDocument &document);
+
+ int writeBlock(const QTextBlock &block, bool table, bool ignoreFormat);
+ void writeFrame(const QTextFrame *frame);
+
+private:
+ QTextStream &m_stream;
+ QTextDocument::MarkdownFeatures m_features;
+};
+
+QT_END_NAMESPACE
+
+#endif // QTEXTMARKDOWNWRITER_P_H
diff --git a/src/gui/text/text.pri b/src/gui/text/text.pri
index b35a231747..5e97b312f1 100644
--- a/src/gui/text/text.pri
+++ b/src/gui/text/text.pri
@@ -109,6 +109,13 @@ qtConfig(textmarkdownreader) {
text/qtextmarkdownimporter.cpp
}
+qtConfig(textmarkdownwriter) {
+ HEADERS += \
+ text/qtextmarkdownwriter_p.h
+ SOURCES += \
+ text/qtextmarkdownwriter.cpp
+}
+
qtConfig(cssparser) {
HEADERS += \
text/qcssparser_p.h