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>
Shawn Rutledge 2019-04-26 07:40:34 +02:00
parent 6a58a68ae3
commit 040dd7fe26
10 changed files with 410 additions and 48 deletions

View File

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

View File

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

View File

@ -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("![image](") + 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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,6 +28,7 @@ SUBDIRS=\
win32:SUBDIRS -= qtextpiecetable
qtConfig(textmarkdownreader): SUBDIRS += qtextmarkdownimporter
qtConfig(textmarkdownwriter): SUBDIRS += qtextmarkdownwriter
!qtConfig(private_tests): SUBDIRS -= \