Markdown: fix several issues with lists and continuations
Importer fixes: - the first list item after a heading doesn't keep the heading font - the first text fragment after a bullet is the bullet text, not a separate paragraph - detect continuation lines and append to the list item text - detect continuation paragraphs and indent them properly - indent nested list items properly - add a test for QTextMarkdownImporter Writer fixes: - after bullet items, continuation lines and paragraphs are indented - indentation of continuations isn't affected by checkboxes - add extra newlines between list items in "loose" lists - avoid writing triple newlines - enhance the test for QTextMarkdownWriter Change-Id: Ib1dda514832f6dc0cdad177aa9a423a7038ac8c6 Reviewed-by: Gatis Paeglis <gatis.paeglis@qt.io>
parent
6a58a68ae3
commit
040dd7fe26
|
|
@ -52,6 +52,9 @@ QT_BEGIN_NAMESPACE
|
|||
|
||||
Q_LOGGING_CATEGORY(lcMD, "qt.text.markdown")
|
||||
|
||||
static const QChar Newline = QLatin1Char('\n');
|
||||
static const QChar Space = QLatin1Char(' ');
|
||||
|
||||
// --------------------------------------------------------
|
||||
// MD4C callback function wrappers
|
||||
|
||||
|
|
@ -141,18 +144,33 @@ int QTextMarkdownImporter::cbEnterBlock(int blockType, void *det)
|
|||
{
|
||||
m_blockType = blockType;
|
||||
switch (blockType) {
|
||||
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_P:
|
||||
if (m_listStack.isEmpty()) {
|
||||
QTextBlockFormat blockFmt;
|
||||
int margin = m_doc->defaultFont().pointSize() / 2;
|
||||
blockFmt.setTopMargin(margin);
|
||||
blockFmt.setBottomMargin(margin);
|
||||
m_cursor->insertBlock(blockFmt, QTextCharFormat());
|
||||
qCDebug(lcMD, "P");
|
||||
} else {
|
||||
if (m_emptyListItem) {
|
||||
qCDebug(lcMD, "LI text block at level %d -> BlockIndent %d",
|
||||
m_listStack.count(), m_cursor->blockFormat().indent());
|
||||
m_emptyListItem = false;
|
||||
} else {
|
||||
qCDebug(lcMD, "P inside LI at level %d", m_listStack.count());
|
||||
QTextBlockFormat blockFmt;
|
||||
blockFmt.setIndent(m_listStack.count());
|
||||
m_cursor->insertBlock(blockFmt, QTextCharFormat());
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MD_BLOCK_CODE: {
|
||||
QTextBlockFormat blockFmt;
|
||||
QTextCharFormat charFmt;
|
||||
charFmt.setFont(m_monoFont);
|
||||
m_cursor->insertBlock(blockFmt, charFmt);
|
||||
qCDebug(lcMD, "CODE");
|
||||
} break;
|
||||
case MD_BLOCK_H: {
|
||||
MD_BLOCK_H_DETAIL *detail = static_cast<MD_BLOCK_H_DETAIL *>(det);
|
||||
|
|
@ -163,6 +181,7 @@ int QTextMarkdownImporter::cbEnterBlock(int blockType, void *det)
|
|||
charFmt.setFontWeight(QFont::Bold);
|
||||
blockFmt.setHeadingLevel(int(detail->level));
|
||||
m_cursor->insertBlock(blockFmt, charFmt);
|
||||
qCDebug(lcMD, "H%d", detail->level);
|
||||
} break;
|
||||
case MD_BLOCK_LI: {
|
||||
MD_BLOCK_LI_DETAIL *detail = static_cast<MD_BLOCK_LI_DETAIL *>(det);
|
||||
|
|
@ -176,7 +195,10 @@ int QTextMarkdownImporter::cbEnterBlock(int blockType, void *det)
|
|||
list->add(m_cursor->block());
|
||||
}
|
||||
m_cursor->setBlockFormat(bfmt);
|
||||
qCDebug(lcMD) << (m_emptyList ? "LI (first in list)" : "LI");
|
||||
m_emptyList = false; // Avoid insertBlock for the first item (because insertList already did that)
|
||||
m_listItem = true;
|
||||
m_emptyListItem = true;
|
||||
} break;
|
||||
case MD_BLOCK_UL: {
|
||||
MD_BLOCK_UL_DETAIL *detail = static_cast<MD_BLOCK_UL_DETAIL *>(det);
|
||||
|
|
@ -193,6 +215,7 @@ int QTextMarkdownImporter::cbEnterBlock(int blockType, void *det)
|
|||
fmt.setStyle(QTextListFormat::ListDisc);
|
||||
break;
|
||||
}
|
||||
qCDebug(lcMD, "UL %c level %d", detail->mark, m_listStack.count());
|
||||
m_listStack.push(m_cursor->insertList(fmt));
|
||||
m_emptyList = true;
|
||||
} break;
|
||||
|
|
@ -202,6 +225,7 @@ int QTextMarkdownImporter::cbEnterBlock(int blockType, void *det)
|
|||
fmt.setIndent(m_listStack.count() + 1);
|
||||
fmt.setNumberSuffix(QChar::fromLatin1(detail->mark_delimiter));
|
||||
fmt.setStyle(QTextListFormat::ListDecimal);
|
||||
qCDebug(lcMD, "OL xx%d level %d", detail->mark_delimiter, m_listStack.count());
|
||||
m_listStack.push(m_cursor->insertList(fmt));
|
||||
m_emptyList = true;
|
||||
} break;
|
||||
|
|
@ -265,6 +289,7 @@ int QTextMarkdownImporter::cbLeaveBlock(int blockType, void *detail)
|
|||
switch (blockType) {
|
||||
case MD_BLOCK_UL:
|
||||
case MD_BLOCK_OL:
|
||||
qCDebug(lcMD, "list at level %d ended", m_listStack.count());
|
||||
m_listStack.pop();
|
||||
break;
|
||||
case MD_BLOCK_TR: {
|
||||
|
|
@ -299,6 +324,14 @@ int QTextMarkdownImporter::cbLeaveBlock(int blockType, void *detail)
|
|||
m_currentTable = nullptr;
|
||||
m_cursor->movePosition(QTextCursor::End);
|
||||
break;
|
||||
case MD_BLOCK_LI:
|
||||
qCDebug(lcMD, "LI at level %d ended", m_listStack.count());
|
||||
m_listItem = false;
|
||||
break;
|
||||
case MD_BLOCK_CODE:
|
||||
case MD_BLOCK_H:
|
||||
m_cursor->setCharFormat(QTextCharFormat());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -381,10 +414,10 @@ int QTextMarkdownImporter::cbText(int textType, const char *text, unsigned size)
|
|||
s = QString(QChar(0xFFFD)); // CommonMark-required replacement for null
|
||||
break;
|
||||
case MD_TEXT_BR:
|
||||
s = QLatin1String("\n");
|
||||
s = QString(Newline);
|
||||
break;
|
||||
case MD_TEXT_SOFTBR:
|
||||
s = QLatin1String(" ");
|
||||
s = QString(Space);
|
||||
break;
|
||||
case MD_TEXT_CODE:
|
||||
// We'll see MD_SPAN_CODE too, which will set the char format, and that's enough.
|
||||
|
|
@ -431,6 +464,14 @@ int QTextMarkdownImporter::cbText(int textType, const char *text, unsigned size)
|
|||
|
||||
if (!s.isEmpty())
|
||||
m_cursor->insertText(s);
|
||||
if (m_cursor->currentList()) {
|
||||
// The list item will indent the list item's text, so we don't need indentation on the block.
|
||||
QTextBlockFormat blockFmt = m_cursor->blockFormat();
|
||||
blockFmt.setIndent(0);
|
||||
m_cursor->setBlockFormat(blockFmt);
|
||||
}
|
||||
qCDebug(lcMD) << textType << "in block" << m_blockType << s << "in list?" << m_cursor->currentList()
|
||||
<< "indent" << m_cursor->blockFormat().indent();
|
||||
return 0; // no error
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -116,6 +116,8 @@ private:
|
|||
Features m_features;
|
||||
int m_blockType = 0;
|
||||
bool m_emptyList = false; // true when the last thing we did was insertList
|
||||
bool m_listItem = false;
|
||||
bool m_emptyListItem = false;
|
||||
bool m_imageSpan = false;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -46,12 +46,17 @@
|
|||
#include "qtexttable.h"
|
||||
#include "qtextcursor.h"
|
||||
#include "qtextimagehandler_p.h"
|
||||
#include "qloggingcategory.h"
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
Q_LOGGING_CATEGORY(lcMDW, "qt.text.markdown.writer")
|
||||
|
||||
static const QChar Space = QLatin1Char(' ');
|
||||
static const QChar Newline = QLatin1Char('\n');
|
||||
static const QChar LineBreak = QChar(0x2028);
|
||||
static const QChar Backtick = QLatin1Char('`');
|
||||
static const QChar Period = QLatin1Char('.');
|
||||
|
||||
QTextMarkdownWriter::QTextMarkdownWriter(QTextStream &stream, QTextDocument::MarkdownFeatures features)
|
||||
: m_stream(stream), m_features(features)
|
||||
|
|
@ -93,6 +98,7 @@ void QTextMarkdownWriter::writeTable(const QAbstractTableModel &table)
|
|||
}
|
||||
m_stream << '|'<< Qt::endl;
|
||||
}
|
||||
m_listInfo.clear();
|
||||
}
|
||||
|
||||
void QTextMarkdownWriter::writeFrame(const QTextFrame *frame)
|
||||
|
|
@ -144,6 +150,7 @@ void QTextMarkdownWriter::writeFrame(const QTextFrame *frame)
|
|||
m_stream << Newline;
|
||||
}
|
||||
int endingCol = writeBlock(block, !table, table && tableRow == 0);
|
||||
m_doubleNewlineWritten = false;
|
||||
if (table) {
|
||||
QTextTableCell cell = table->cellAt(block.position());
|
||||
int paddingLen = -endingCol;
|
||||
|
|
@ -158,14 +165,48 @@ void QTextMarkdownWriter::writeFrame(const QTextFrame *frame)
|
|||
m_stream << Newline;
|
||||
} else if (endingCol > 0) {
|
||||
m_stream << Newline << Newline;
|
||||
m_doubleNewlineWritten = true;
|
||||
}
|
||||
lastWasList = block.textList();
|
||||
}
|
||||
child = iterator.currentFrame();
|
||||
++iterator;
|
||||
}
|
||||
if (table)
|
||||
if (table) {
|
||||
m_stream << Newline << Newline;
|
||||
m_doubleNewlineWritten = true;
|
||||
}
|
||||
m_listInfo.clear();
|
||||
}
|
||||
|
||||
QTextMarkdownWriter::ListInfo QTextMarkdownWriter::listInfo(QTextList *list)
|
||||
{
|
||||
if (!m_listInfo.contains(list)) {
|
||||
// decide whether this list is loose or tight
|
||||
ListInfo info;
|
||||
info.loose = false;
|
||||
if (list->count() > 1) {
|
||||
QTextBlock first = list->item(0);
|
||||
QTextBlock last = list->item(list->count() - 1);
|
||||
QTextBlock next = first.next();
|
||||
while (next.isValid()) {
|
||||
if (next == last)
|
||||
break;
|
||||
qCDebug(lcMDW) << "next block in list" << list << next.text() << "part of list?" << next.textList();
|
||||
if (!next.textList()) {
|
||||
// If we find a continuation paragraph, this list is "loose"
|
||||
// because it will need a blank line to separate that paragraph.
|
||||
qCDebug(lcMDW) << "decided list beginning with" << first.text() << "is loose after" << next.text();
|
||||
info.loose = true;
|
||||
break;
|
||||
}
|
||||
next = next.next();
|
||||
}
|
||||
}
|
||||
m_listInfo.insert(list, info);
|
||||
return info;
|
||||
}
|
||||
return m_listInfo.value(list);
|
||||
}
|
||||
|
||||
static int nearestWordWrapIndex(const QString &s, int before)
|
||||
|
|
@ -211,7 +252,6 @@ static void maybeEscapeFirstChar(QString &s)
|
|||
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();
|
||||
|
|
@ -219,9 +259,18 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
|
|||
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::ListDisc:
|
||||
bullet = "-";
|
||||
m_wrappedLineIndent = 2;
|
||||
break;
|
||||
case QTextListFormat::ListCircle:
|
||||
bullet = "*";
|
||||
m_wrappedLineIndent = 2;
|
||||
break;
|
||||
case QTextListFormat::ListSquare:
|
||||
bullet = "+";
|
||||
m_wrappedLineIndent = 2;
|
||||
break;
|
||||
case QTextListFormat::ListStyleUndefined: break;
|
||||
case QTextListFormat::ListDecimal:
|
||||
case QTextListFormat::ListLowerAlpha:
|
||||
|
|
@ -229,6 +278,7 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
|
|||
case QTextListFormat::ListLowerRoman:
|
||||
case QTextListFormat::ListUpperRoman:
|
||||
numeric = true;
|
||||
m_wrappedLineIndent = 4;
|
||||
break;
|
||||
}
|
||||
switch (block.blockFormat().marker()) {
|
||||
|
|
@ -241,23 +291,36 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
|
|||
default:
|
||||
break;
|
||||
}
|
||||
QString prefix((listLevel - 1) * (numeric ? 4 : 2), Space);
|
||||
if (numeric)
|
||||
prefix += QString::number(number) + fmt.numberSuffix() + Space;
|
||||
else
|
||||
int indentFirstLine = (listLevel - 1) * (numeric ? 4 : 2);
|
||||
m_wrappedLineIndent += indentFirstLine;
|
||||
if (m_lastListIndent != listLevel && !m_doubleNewlineWritten && listInfo(block.textList()).loose)
|
||||
m_stream << Newline;
|
||||
m_lastListIndent = listLevel;
|
||||
QString prefix(indentFirstLine, Space);
|
||||
if (numeric) {
|
||||
QString suffix = fmt.numberSuffix();
|
||||
if (suffix.isEmpty())
|
||||
suffix = QString(Period);
|
||||
QString numberStr = QString::number(number) + suffix + Space;
|
||||
if (numberStr.length() == 3)
|
||||
numberStr += Space;
|
||||
prefix += numberStr;
|
||||
} else {
|
||||
prefix += QLatin1String(bullet) + Space;
|
||||
}
|
||||
m_stream << prefix;
|
||||
wrapIndent = prefix.length();
|
||||
} else if (!block.blockFormat().indent()) {
|
||||
m_wrappedLineIndent = 0;
|
||||
}
|
||||
|
||||
if (block.blockFormat().headingLevel())
|
||||
m_stream << QByteArray(block.blockFormat().headingLevel(), '#') << ' ';
|
||||
|
||||
QString wrapIndentString(wrapIndent, Space);
|
||||
QString wrapIndentString(m_wrappedLineIndent, 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;
|
||||
int col = m_wrappedLineIndent;
|
||||
bool mono = false;
|
||||
bool startsOrEndsWithBacktick = false;
|
||||
bool bold = false;
|
||||
|
|
@ -267,8 +330,16 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
|
|||
QString backticks(Backtick);
|
||||
for (QTextBlock::Iterator frag = block.begin(); !frag.atEnd(); ++frag) {
|
||||
QString fragmentText = frag.fragment().text();
|
||||
while (fragmentText.endsWith(QLatin1Char('\n')))
|
||||
while (fragmentText.endsWith(Newline))
|
||||
fragmentText.chop(1);
|
||||
if (block.textList()) { // <li>first line</br>continuation</li>
|
||||
QString newlineIndent = QString(Newline) + QString(m_wrappedLineIndent, Space);
|
||||
fragmentText.replace(QString(LineBreak), newlineIndent);
|
||||
} else if (block.blockFormat().indent() > 0) { // <li>first line<p>continuation</p></li>
|
||||
m_stream << QString(m_wrappedLineIndent, Space);
|
||||
} else {
|
||||
fragmentText.replace(LineBreak, Newline);
|
||||
}
|
||||
startsOrEndsWithBacktick |= fragmentText.startsWith(Backtick) || fragmentText.endsWith(Backtick);
|
||||
QTextCharFormat fmt = frag.fragment().charFormat();
|
||||
if (fmt.isImageFormat()) {
|
||||
|
|
@ -276,7 +347,7 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
|
|||
QString s = QLatin1String(" + ifmt.name() + QLatin1Char(')');
|
||||
if (wrap && col + s.length() > ColumnLimit) {
|
||||
m_stream << Newline << wrapIndentString;
|
||||
col = wrapIndent;
|
||||
col = m_wrappedLineIndent;
|
||||
}
|
||||
m_stream << s;
|
||||
col += s.length();
|
||||
|
|
@ -285,7 +356,7 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
|
|||
fmt.property(QTextFormat::AnchorHref).toString() + QLatin1Char(')');
|
||||
if (wrap && col + s.length() > ColumnLimit) {
|
||||
m_stream << Newline << wrapIndentString;
|
||||
col = wrapIndent;
|
||||
col = m_wrappedLineIndent;
|
||||
}
|
||||
m_stream << s;
|
||||
col += s.length();
|
||||
|
|
@ -296,7 +367,7 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
|
|||
if (!ignoreFormat) {
|
||||
if (monoFrag != mono) {
|
||||
if (monoFrag)
|
||||
backticks = QString::fromLatin1(QByteArray(adjacentBackticksCount(fragmentText) + 1, '`'));
|
||||
backticks = QString(adjacentBackticksCount(fragmentText) + 1, Backtick);
|
||||
markers += backticks;
|
||||
if (startsOrEndsWithBacktick)
|
||||
markers += Space;
|
||||
|
|
@ -347,12 +418,12 @@ int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ign
|
|||
m_stream << markers;
|
||||
col += markers.length();
|
||||
}
|
||||
if (col == wrapIndent)
|
||||
if (col == m_wrappedLineIndent)
|
||||
maybeEscapeFirstChar(subfrag);
|
||||
m_stream << subfrag;
|
||||
if (breakingLine) {
|
||||
m_stream << Newline << wrapIndentString;
|
||||
col = wrapIndent;
|
||||
col = m_wrappedLineIndent;
|
||||
} else {
|
||||
col += subfrag.length();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,9 +70,20 @@ public:
|
|||
int writeBlock(const QTextBlock &block, bool table, bool ignoreFormat);
|
||||
void writeFrame(const QTextFrame *frame);
|
||||
|
||||
private:
|
||||
struct ListInfo {
|
||||
bool loose;
|
||||
};
|
||||
|
||||
ListInfo listInfo(QTextList *list);
|
||||
|
||||
private:
|
||||
QTextStream &m_stream;
|
||||
QTextDocument::MarkdownFeatures m_features;
|
||||
QMap<QTextList *, ListInfo> m_listInfo;
|
||||
int m_wrappedLineIndent = 0;
|
||||
int m_lastListIndent = 1;
|
||||
bool m_doubleNewlineWritten = false;
|
||||
};
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
# heading
|
||||
- bullet 1
|
||||
continuation line 1, indented via tab
|
||||
- bullet 2
|
||||
continuation line 2, indented via 4 spaces
|
||||
- bullet 3
|
||||
|
||||
continuation paragraph 3, indented via tab
|
||||
|
||||
- bullet 3.1
|
||||
|
||||
continuation paragraph 3.1, indented via 4 spaces
|
||||
|
||||
- bullet 3.2
|
||||
continuation line, indented via 2 tabs
|
||||
- bullet 4
|
||||
|
||||
continuation paragraph 4, indented via 4 spaces
|
||||
and continuing onto another line too
|
||||
|
||||
- bullet 5
|
||||
|
||||
continuation paragraph 5, indented via 2 spaces and continuing onto another
|
||||
line too
|
||||
|
||||
- bullet 6
|
||||
|
||||
plain old paragraph at the end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
CONFIG += testcase
|
||||
TARGET = tst_qtextmarkdownimporter
|
||||
QT += core-private gui-private testlib
|
||||
SOURCES += tst_qtextmarkdownimporter.cpp
|
||||
TESTDATA += data/headingBulletsContinuations.md
|
||||
|
||||
DEFINES += SRCDIR=\\\"$$PWD\\\"
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
/****************************************************************************
|
||||
**
|
||||
** 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/qtextmarkdownimporter_p.h>
|
||||
|
||||
// #define DEBUG_WRITE_HTML
|
||||
|
||||
Q_LOGGING_CATEGORY(lcTests, "qt.text.tests")
|
||||
|
||||
static const QChar LineBreak = QChar(0x2028);
|
||||
static const QChar Tab = QLatin1Char('\t');
|
||||
static const QChar Space = QLatin1Char(' ');
|
||||
static const QChar Period = QLatin1Char('.');
|
||||
|
||||
class tst_QTextMarkdownImporter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void headingBulletsContinuations();
|
||||
};
|
||||
|
||||
void tst_QTextMarkdownImporter::headingBulletsContinuations()
|
||||
{
|
||||
const QStringList expectedBlocks = QStringList() <<
|
||||
"" << // we could do without this blank line before the heading, but currently it happens
|
||||
"heading" <<
|
||||
"bullet 1 continuation line 1, indented via tab" <<
|
||||
"bullet 2 continuation line 2, indented via 4 spaces" <<
|
||||
"bullet 3" <<
|
||||
"continuation paragraph 3, indented via tab" <<
|
||||
"bullet 3.1" <<
|
||||
"continuation paragraph 3.1, indented via 4 spaces" <<
|
||||
"bullet 3.2 continuation line, indented via 2 tabs" <<
|
||||
"bullet 4" <<
|
||||
"continuation paragraph 4, indented via 4 spaces and continuing onto another line too" <<
|
||||
"bullet 5" <<
|
||||
// indenting by only 2 spaces is perhaps non-standard but currently is OK
|
||||
"continuation paragraph 5, indented via 2 spaces and continuing onto another line too" <<
|
||||
"bullet 6" <<
|
||||
"plain old paragraph at the end";
|
||||
|
||||
QFile f(QFINDTESTDATA("data/headingBulletsContinuations.md"));
|
||||
QVERIFY(f.open(QFile::ReadOnly | QIODevice::Text));
|
||||
QString md = QString::fromUtf8(f.readAll());
|
||||
f.close();
|
||||
|
||||
QTextDocument doc;
|
||||
QTextMarkdownImporter(QTextMarkdownImporter::DialectGitHub).import(&doc, md);
|
||||
QTextFrame::iterator iterator = doc.rootFrame()->begin();
|
||||
QTextFrame *currentFrame = iterator.currentFrame();
|
||||
QStringList::const_iterator expectedIt = expectedBlocks.constBegin();
|
||||
int i = 0;
|
||||
while (!iterator.atEnd()) {
|
||||
// There are no child frames
|
||||
QCOMPARE(iterator.currentFrame(), currentFrame);
|
||||
// Check whether we got the right child block
|
||||
QTextBlock block = iterator.currentBlock();
|
||||
QCOMPARE(block.text().contains(LineBreak), false);
|
||||
QCOMPARE(block.text().contains(Tab), false);
|
||||
QVERIFY(!block.text().startsWith(Space));
|
||||
int expectedIndentation = 0;
|
||||
if (block.text().contains(QLatin1String("continuation paragraph")))
|
||||
expectedIndentation = (block.text().contains(Period) ? 2 : 1);
|
||||
qCDebug(lcTests) << i << "child block" << block.text() << "indentation" << block.blockFormat().indent();
|
||||
QVERIFY(expectedIt != expectedBlocks.constEnd());
|
||||
QCOMPARE(block.text(), *expectedIt);
|
||||
if (i > 2)
|
||||
QCOMPARE(block.blockFormat().indent(), expectedIndentation);
|
||||
++iterator;
|
||||
++expectedIt;
|
||||
++i;
|
||||
}
|
||||
QCOMPARE(expectedIt, expectedBlocks.constEnd());
|
||||
|
||||
#ifdef DEBUG_WRITE_HTML
|
||||
{
|
||||
QFile out("/tmp/headingBulletsContinuations.html");
|
||||
out.open(QFile::WriteOnly);
|
||||
out.write(doc.toHtml().toLatin1());
|
||||
out.close();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
QTEST_MAIN(tst_QTextMarkdownImporter)
|
||||
#include "tst_qtextmarkdownimporter.moc"
|
||||
|
|
@ -27,8 +27,8 @@ text layout changes.*
|
|||
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
|
||||
- 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.
|
||||
|
||||
|
|
@ -36,13 +36,13 @@ 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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ private slots:
|
|||
void testWriteParagraph_data();
|
||||
void testWriteParagraph();
|
||||
void testWriteList();
|
||||
void testWriteNestedBulletLists_data();
|
||||
void testWriteNestedBulletLists();
|
||||
void testWriteNestedNumericLists();
|
||||
void testWriteTable();
|
||||
|
|
@ -122,9 +123,44 @@ void tst_QTextMarkdownWriter::testWriteList()
|
|||
"- ListItem 1\n- ListItem 2\n"));
|
||||
}
|
||||
|
||||
void tst_QTextMarkdownWriter::testWriteNestedBulletLists_data()
|
||||
{
|
||||
QTest::addColumn<bool>("checkbox");
|
||||
QTest::addColumn<bool>("checked");
|
||||
QTest::addColumn<bool>("continuationLine");
|
||||
QTest::addColumn<bool>("continuationParagraph");
|
||||
QTest::addColumn<QString>("expectedOutput");
|
||||
|
||||
QTest::newRow("plain bullets") << false << false << false << false <<
|
||||
"- ListItem 1\n * ListItem 2\n + ListItem 3\n- ListItem 4\n * ListItem 5\n";
|
||||
QTest::newRow("bullets with continuation lines") << false << false << true << false <<
|
||||
"- ListItem 1\n * ListItem 2\n + ListItem 3 with text that won't fit on one line and thus needs a\n continuation\n- ListItem 4\n * ListItem 5 with text that won't fit on one line and thus needs a\n continuation\n";
|
||||
QTest::newRow("bullets with continuation paragraphs") << false << false << false << true <<
|
||||
"- ListItem 1\n\n * ListItem 2\n + ListItem 3\n\n continuation\n\n- ListItem 4\n\n * ListItem 5\n\n continuation\n\n";
|
||||
QTest::newRow("unchecked") << true << false << false << false <<
|
||||
"- [ ] ListItem 1\n * [ ] ListItem 2\n + [ ] ListItem 3\n- [ ] ListItem 4\n * [ ] ListItem 5\n";
|
||||
QTest::newRow("checked") << true << true << false << false <<
|
||||
"- [x] ListItem 1\n * [x] ListItem 2\n + [x] ListItem 3\n- [x] ListItem 4\n * [x] ListItem 5\n";
|
||||
QTest::newRow("checked with continuation lines") << true << true << true << false <<
|
||||
"- [x] ListItem 1\n * [x] ListItem 2\n + [x] ListItem 3 with text that won't fit on one line and thus needs a\n continuation\n- [x] ListItem 4\n * [x] ListItem 5 with text that won't fit on one line and thus needs a\n continuation\n";
|
||||
QTest::newRow("checked with continuation paragraphs") << true << true << false << true <<
|
||||
"- [x] ListItem 1\n\n * [x] ListItem 2\n + [x] ListItem 3\n\n continuation\n\n- [x] ListItem 4\n\n * [x] ListItem 5\n\n continuation\n\n";
|
||||
}
|
||||
|
||||
void tst_QTextMarkdownWriter::testWriteNestedBulletLists()
|
||||
{
|
||||
QFETCH(bool, checkbox);
|
||||
QFETCH(bool, checked);
|
||||
QFETCH(bool, continuationParagraph);
|
||||
QFETCH(bool, continuationLine);
|
||||
QFETCH(QString, expectedOutput);
|
||||
|
||||
QTextCursor cursor(document);
|
||||
QTextBlockFormat blockFmt = cursor.blockFormat();
|
||||
if (checkbox) {
|
||||
blockFmt.setMarker(checked ? QTextBlockFormat::Checked : QTextBlockFormat::Unchecked);
|
||||
cursor.setBlockFormat(blockFmt);
|
||||
}
|
||||
|
||||
QTextList *list1 = cursor.createList(QTextListFormat::ListDisc);
|
||||
cursor.insertText("ListItem 1");
|
||||
|
|
@ -140,18 +176,42 @@ void tst_QTextMarkdownWriter::testWriteNestedBulletLists()
|
|||
fmt3.setStyle(QTextListFormat::ListSquare);
|
||||
fmt3.setIndent(3);
|
||||
cursor.insertList(fmt3);
|
||||
cursor.insertText("ListItem 3");
|
||||
cursor.insertText(continuationLine ?
|
||||
"ListItem 3 with text that won't fit on one line and thus needs a continuation" :
|
||||
"ListItem 3");
|
||||
if (continuationParagraph) {
|
||||
QTextBlockFormat blockFmt;
|
||||
blockFmt.setIndent(2);
|
||||
cursor.insertBlock(blockFmt);
|
||||
cursor.insertText("continuation");
|
||||
}
|
||||
|
||||
cursor.insertBlock();
|
||||
cursor.insertBlock(blockFmt);
|
||||
cursor.insertText("ListItem 4");
|
||||
list1->add(cursor.block());
|
||||
|
||||
cursor.insertBlock();
|
||||
cursor.insertText("ListItem 5");
|
||||
cursor.insertText(continuationLine ?
|
||||
"ListItem 5 with text that won't fit on one line and thus needs a continuation" :
|
||||
"ListItem 5");
|
||||
list2->add(cursor.block());
|
||||
if (continuationParagraph) {
|
||||
QTextBlockFormat blockFmt;
|
||||
blockFmt.setIndent(2);
|
||||
cursor.insertBlock(blockFmt);
|
||||
cursor.insertText("continuation");
|
||||
}
|
||||
|
||||
QCOMPARE(documentToUnixMarkdown(), QString::fromLatin1(
|
||||
"- ListItem 1\n * ListItem 2\n + ListItem 3\n- ListItem 4\n * ListItem 5\n"));
|
||||
QString output = documentToUnixMarkdown();
|
||||
#ifdef DEBUG_WRITE_OUTPUT
|
||||
{
|
||||
QFile out("/tmp/" + QLatin1String(QTest::currentDataTag()) + ".md");
|
||||
out.open(QFile::WriteOnly);
|
||||
out.write(output.toUtf8());
|
||||
out.close();
|
||||
}
|
||||
#endif
|
||||
QCOMPARE(documentToUnixMarkdown(), expectedOutput);
|
||||
}
|
||||
|
||||
void tst_QTextMarkdownWriter::testWriteNestedNumericLists()
|
||||
|
|
@ -185,7 +245,7 @@ void tst_QTextMarkdownWriter::testWriteNestedNumericLists()
|
|||
|
||||
// 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"));
|
||||
"1. ListItem 1\n 1) ListItem 2\n 1. ListItem 3\n2. ListItem 4\n 2) ListItem 5\n"));
|
||||
}
|
||||
|
||||
void tst_QTextMarkdownWriter::testWriteTable()
|
||||
|
|
@ -312,8 +372,8 @@ void tst_QTextMarkdownWriter::rewriteDocument()
|
|||
|
||||
void tst_QTextMarkdownWriter::fromHtml_data()
|
||||
{
|
||||
QTest::addColumn<QString>("input");
|
||||
QTest::addColumn<QString>("output");
|
||||
QTest::addColumn<QString>("expectedInput");
|
||||
QTest::addColumn<QString>("expectedOutput");
|
||||
|
||||
QTest::newRow("long URL") <<
|
||||
"<span style=\"font-style:italic;\">https://www.example.com/dir/subdir/subsubdir/subsubsubdir/subsubsubsubdir/subsubsubsubsubdir/</span>" <<
|
||||
|
|
@ -329,6 +389,15 @@ void tst_QTextMarkdownWriter::fromHtml_data()
|
|||
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";
|
||||
QTest::newRow("list items with indented continuations") <<
|
||||
"<ul><li>bullet<p>continuation paragraph</p></li><li>another bullet<br/>continuation line</li></ul>" <<
|
||||
"- bullet\n\n continuation paragraph\n\n- another bullet\n continuation line\n";
|
||||
QTest::newRow("nested list items with continuations") <<
|
||||
"<ul><li>bullet<p>continuation paragraph</p></li><li>another bullet<br/>continuation line</li><ul><li>bullet<p>continuation paragraph</p></li><li>another bullet<br/>continuation line</li></ul></ul>" <<
|
||||
"- bullet\n\n continuation paragraph\n\n- another bullet\n continuation line\n\n - bullet\n\n continuation paragraph\n\n - another bullet\n continuation line\n";
|
||||
QTest::newRow("nested ordered list items with continuations") <<
|
||||
"<ol><li>item<p>continuation paragraph</p></li><li>another item<br/>continuation line</li><ol><li>item<p>continuation paragraph</p></li><li>another item<br/>continuation line</li></ol><li>another</li><li>another</li></ol>" <<
|
||||
"1. item\n\n continuation paragraph\n\n2. another item\n continuation line\n\n 1. item\n\n continuation paragraph\n\n 2. another item\n continuation line\n\n3. another\n4. another\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" <<
|
||||
|
|
@ -340,11 +409,22 @@ void tst_QTextMarkdownWriter::fromHtml_data()
|
|||
|
||||
void tst_QTextMarkdownWriter::fromHtml()
|
||||
{
|
||||
QFETCH(QString, input);
|
||||
QFETCH(QString, output);
|
||||
QFETCH(QString, expectedInput);
|
||||
QFETCH(QString, expectedOutput);
|
||||
|
||||
document->setHtml(input);
|
||||
QCOMPARE(documentToUnixMarkdown(), output);
|
||||
document->setHtml(expectedInput);
|
||||
QString output = documentToUnixMarkdown();
|
||||
|
||||
#ifdef DEBUG_WRITE_OUTPUT
|
||||
{
|
||||
QFile out("/tmp/" + QLatin1String(QTest::currentDataTag()) + ".md");
|
||||
out.open(QFile::WriteOnly);
|
||||
out.write(output.toUtf8());
|
||||
out.close();
|
||||
}
|
||||
#endif
|
||||
|
||||
QCOMPARE(output, expectedOutput);
|
||||
}
|
||||
|
||||
QString tst_QTextMarkdownWriter::documentToUnixMarkdown()
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ SUBDIRS=\
|
|||
|
||||
win32:SUBDIRS -= qtextpiecetable
|
||||
|
||||
qtConfig(textmarkdownreader): SUBDIRS += qtextmarkdownimporter
|
||||
qtConfig(textmarkdownwriter): SUBDIRS += qtextmarkdownwriter
|
||||
|
||||
!qtConfig(private_tests): SUBDIRS -= \
|
||||
|
|
|
|||
Loading…
Reference in New Issue