summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorShawn Rutledge <shawn.rutledge@qt.io>2019-04-26 07:40:34 +0200
committerShawn Rutledge <shawn.rutledge@qt.io>2019-05-08 20:28:28 +0000
commit040dd7fe26bfa34aae19e2db698ff0d69346ed56 (patch)
tree180b60f5a9e171609fadabf6e36b81ad3337b2d0 /src
parent6a58a68ae3feb27dca48ebb3274b748f784b3d12 (diff)
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>
Diffstat (limited to 'src')
-rw-r--r--src/gui/text/qtextmarkdownimporter.cpp59
-rw-r--r--src/gui/text/qtextmarkdownimporter_p.h2
-rw-r--r--src/gui/text/qtextmarkdownwriter.cpp107
-rw-r--r--src/gui/text/qtextmarkdownwriter_p.h11
4 files changed, 152 insertions, 27 deletions
diff --git a/src/gui/text/qtextmarkdownimporter.cpp b/src/gui/text/qtextmarkdownimporter.cpp
index 2c520a71c9..a65d8f270e 100644
--- a/src/gui/text/qtextmarkdownimporter.cpp
+++ b/src/gui/text/qtextmarkdownimporter.cpp
@@ -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
}
diff --git a/src/gui/text/qtextmarkdownimporter_p.h b/src/gui/text/qtextmarkdownimporter_p.h
index dee24a8e22..8ab119d051 100644
--- a/src/gui/text/qtextmarkdownimporter_p.h
+++ b/src/gui/text/qtextmarkdownimporter_p.h
@@ -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;
};
diff --git a/src/gui/text/qtextmarkdownwriter.cpp b/src/gui/text/qtextmarkdownwriter.cpp
index 313d62bb8a..2f4c8587ad 100644
--- a/src/gui/text/qtextmarkdownwriter.cpp
+++ b/src/gui/text/qtextmarkdownwriter.cpp
@@ -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();
}
diff --git a/src/gui/text/qtextmarkdownwriter_p.h b/src/gui/text/qtextmarkdownwriter_p.h
index 2a9388ca2d..250288bcff 100644
--- a/src/gui/text/qtextmarkdownwriter_p.h
+++ b/src/gui/text/qtextmarkdownwriter_p.h
@@ -71,8 +71,19 @@ public:
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