1 /****************************************************************************
2 **
3 ** Copyright (C) 2019 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the QtGui module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39 
40 #include "qtextmarkdownimporter_p.h"
41 #include "qtextdocumentfragment_p.h"
42 #include <QLoggingCategory>
43 #if QT_CONFIG(regularexpression)
44 #include <QRegularExpression>
45 #endif
46 #include <QTextCursor>
47 #include <QTextDocument>
48 #include <QTextDocumentFragment>
49 #include <QTextList>
50 #include <QTextTable>
51 #include "../../3rdparty/md4c/md4c.h"
52 
53 QT_BEGIN_NAMESPACE
54 
55 Q_LOGGING_CATEGORY(lcMD, "qt.text.markdown")
56 
57 static const QChar Newline = QLatin1Char('\n');
58 static const QChar Space = QLatin1Char(' ');
59 
60 // TODO maybe eliminate the margins after all views recognize BlockQuoteLevel, CSS can format it, etc.
61 static const int BlockQuoteIndent = 40; // pixels, same as in QTextHtmlParserNode::initializeProperties
62 
63 Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeatureCollapseWhitespace) == MD_FLAG_COLLAPSEWHITESPACE);
64 Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeaturePermissiveATXHeaders) == MD_FLAG_PERMISSIVEATXHEADERS);
65 Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeaturePermissiveURLAutoLinks) == MD_FLAG_PERMISSIVEURLAUTOLINKS);
66 Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeaturePermissiveMailAutoLinks) == MD_FLAG_PERMISSIVEEMAILAUTOLINKS);
67 Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeatureNoIndentedCodeBlocks) == MD_FLAG_NOINDENTEDCODEBLOCKS);
68 Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeatureNoHTMLBlocks) == MD_FLAG_NOHTMLBLOCKS);
69 Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeatureNoHTMLSpans) == MD_FLAG_NOHTMLSPANS);
70 Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeatureTables) == MD_FLAG_TABLES);
71 Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeatureStrikeThrough) == MD_FLAG_STRIKETHROUGH);
72 Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeaturePermissiveWWWAutoLinks) == MD_FLAG_PERMISSIVEWWWAUTOLINKS);
73 Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeaturePermissiveAutoLinks) == MD_FLAG_PERMISSIVEAUTOLINKS);
74 Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeatureTasklists) == MD_FLAG_TASKLISTS);
75 Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeatureNoHTML) == MD_FLAG_NOHTML);
76 Q_STATIC_ASSERT(int(QTextMarkdownImporter::DialectCommonMark) == MD_DIALECT_COMMONMARK);
77 Q_STATIC_ASSERT(int(QTextMarkdownImporter::DialectGitHub) == MD_DIALECT_GITHUB);
78 
79 // --------------------------------------------------------
80 // MD4C callback function wrappers
81 
CbEnterBlock(MD_BLOCKTYPE type,void * detail,void * userdata)82 static int CbEnterBlock(MD_BLOCKTYPE type, void *detail, void *userdata)
83 {
84     QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata);
85     return mdi->cbEnterBlock(int(type), detail);
86 }
87 
CbLeaveBlock(MD_BLOCKTYPE type,void * detail,void * userdata)88 static int CbLeaveBlock(MD_BLOCKTYPE type, void *detail, void *userdata)
89 {
90     QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata);
91     return mdi->cbLeaveBlock(int(type), detail);
92 }
93 
CbEnterSpan(MD_SPANTYPE type,void * detail,void * userdata)94 static int CbEnterSpan(MD_SPANTYPE type, void *detail, void *userdata)
95 {
96     QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata);
97     return mdi->cbEnterSpan(int(type), detail);
98 }
99 
CbLeaveSpan(MD_SPANTYPE type,void * detail,void * userdata)100 static int CbLeaveSpan(MD_SPANTYPE type, void *detail, void *userdata)
101 {
102     QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata);
103     return mdi->cbLeaveSpan(int(type), detail);
104 }
105 
CbText(MD_TEXTTYPE type,const MD_CHAR * text,MD_SIZE size,void * userdata)106 static int CbText(MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size, void *userdata)
107 {
108     QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata);
109     return mdi->cbText(int(type), text, size);
110 }
111 
CbDebugLog(const char * msg,void * userdata)112 static void CbDebugLog(const char *msg, void *userdata)
113 {
114     Q_UNUSED(userdata)
115     qCDebug(lcMD) << msg;
116 }
117 
118 // MD4C callback function wrappers
119 // --------------------------------------------------------
120 
MdAlignment(MD_ALIGN a,Qt::Alignment defaultAlignment=Qt::AlignLeft|Qt::AlignVCenter)121 static Qt::Alignment MdAlignment(MD_ALIGN a, Qt::Alignment defaultAlignment = Qt::AlignLeft | Qt::AlignVCenter)
122 {
123     switch (a) {
124     case MD_ALIGN_LEFT:
125         return Qt::AlignLeft | Qt::AlignVCenter;
126     case MD_ALIGN_CENTER:
127         return Qt::AlignHCenter | Qt::AlignVCenter;
128     case MD_ALIGN_RIGHT:
129         return Qt::AlignRight | Qt::AlignVCenter;
130     default: // including MD_ALIGN_DEFAULT
131         return defaultAlignment;
132     }
133 }
134 
QTextMarkdownImporter(QTextMarkdownImporter::Features features)135 QTextMarkdownImporter::QTextMarkdownImporter(QTextMarkdownImporter::Features features)
136   : m_monoFont(QFontDatabase::systemFont(QFontDatabase::FixedFont))
137   , m_features(features)
138 {
139 }
140 
QTextMarkdownImporter(QTextDocument::MarkdownFeatures features)141 QTextMarkdownImporter::QTextMarkdownImporter(QTextDocument::MarkdownFeatures features)
142   : QTextMarkdownImporter(static_cast<QTextMarkdownImporter::Features>(int(features)))
143 {
144 }
145 
import(QTextDocument * doc,const QString & markdown)146 void QTextMarkdownImporter::import(QTextDocument *doc, const QString &markdown)
147 {
148     MD_PARSER callbacks = {
149         0, // abi_version
150         unsigned(m_features),
151         &CbEnterBlock,
152         &CbLeaveBlock,
153         &CbEnterSpan,
154         &CbLeaveSpan,
155         &CbText,
156         &CbDebugLog,
157         nullptr // syntax
158     };
159     m_doc = doc;
160     m_paragraphMargin = m_doc->defaultFont().pointSize() * 2 / 3;
161     m_cursor = new QTextCursor(doc);
162     doc->clear();
163     if (doc->defaultFont().pointSize() != -1)
164         m_monoFont.setPointSize(doc->defaultFont().pointSize());
165     else
166         m_monoFont.setPixelSize(doc->defaultFont().pixelSize());
167     qCDebug(lcMD) << "default font" << doc->defaultFont() << "mono font" << m_monoFont;
168     QByteArray md = markdown.toUtf8();
169     md_parse(md.constData(), MD_SIZE(md.size()), &callbacks, this);
170     delete m_cursor;
171     m_cursor = nullptr;
172 }
173 
cbEnterBlock(int blockType,void * det)174 int QTextMarkdownImporter::cbEnterBlock(int blockType, void *det)
175 {
176     m_blockType = blockType;
177     switch (blockType) {
178     case MD_BLOCK_P:
179         if (!m_listStack.isEmpty())
180             qCDebug(lcMD, m_listItem ? "P of LI at level %d"  : "P continuation inside LI at level %d", m_listStack.count());
181         else
182             qCDebug(lcMD, "P");
183         m_needsInsertBlock = true;
184         break;
185     case MD_BLOCK_QUOTE:
186         ++m_blockQuoteDepth;
187         qCDebug(lcMD, "QUOTE level %d", m_blockQuoteDepth);
188         break;
189     case MD_BLOCK_CODE: {
190         MD_BLOCK_CODE_DETAIL *detail = static_cast<MD_BLOCK_CODE_DETAIL *>(det);
191         m_codeBlock = true;
192         m_blockCodeLanguage = QLatin1String(detail->lang.text, int(detail->lang.size));
193         m_blockCodeFence = detail->fence_char;
194         QString info = QLatin1String(detail->info.text, int(detail->info.size));
195         m_needsInsertBlock = true;
196         if (m_blockQuoteDepth)
197             qCDebug(lcMD, "CODE lang '%s' info '%s' fenced with '%c' inside QUOTE %d", qPrintable(m_blockCodeLanguage), qPrintable(info), m_blockCodeFence, m_blockQuoteDepth);
198         else
199             qCDebug(lcMD, "CODE lang '%s' info '%s' fenced with '%c'", qPrintable(m_blockCodeLanguage), qPrintable(info), m_blockCodeFence);
200     } break;
201     case MD_BLOCK_H: {
202         MD_BLOCK_H_DETAIL *detail = static_cast<MD_BLOCK_H_DETAIL *>(det);
203         QTextBlockFormat blockFmt;
204         QTextCharFormat charFmt;
205         int sizeAdjustment = 4 - int(detail->level); // H1 to H6: +3 to -2
206         charFmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment);
207         charFmt.setFontWeight(QFont::Bold);
208         blockFmt.setHeadingLevel(int(detail->level));
209         m_needsInsertBlock = false;
210         if (m_doc->isEmpty()) {
211             m_cursor->setBlockFormat(blockFmt);
212             m_cursor->setCharFormat(charFmt);
213         } else {
214             m_cursor->insertBlock(blockFmt, charFmt);
215         }
216         qCDebug(lcMD, "H%d", detail->level);
217     } break;
218     case MD_BLOCK_LI: {
219         m_needsInsertBlock = true;
220         m_listItem = true;
221         MD_BLOCK_LI_DETAIL *detail = static_cast<MD_BLOCK_LI_DETAIL *>(det);
222         m_markerType = detail->is_task ?
223                     (detail->task_mark == ' ' ? QTextBlockFormat::MarkerType::Unchecked : QTextBlockFormat::MarkerType::Checked) :
224                     QTextBlockFormat::MarkerType::NoMarker;
225         qCDebug(lcMD) << "LI";
226     } break;
227     case MD_BLOCK_UL: {
228         if (m_needsInsertList) // list nested in an empty list
229             m_listStack.push(m_cursor->insertList(m_listFormat));
230         else
231             m_needsInsertList = true;
232         MD_BLOCK_UL_DETAIL *detail = static_cast<MD_BLOCK_UL_DETAIL *>(det);
233         m_listFormat = QTextListFormat();
234         m_listFormat.setIndent(m_listStack.count() + 1);
235         switch (detail->mark) {
236         case '*':
237             m_listFormat.setStyle(QTextListFormat::ListCircle);
238             break;
239         case '+':
240             m_listFormat.setStyle(QTextListFormat::ListSquare);
241             break;
242         default: // including '-'
243             m_listFormat.setStyle(QTextListFormat::ListDisc);
244             break;
245         }
246         qCDebug(lcMD, "UL %c level %d", detail->mark, m_listStack.count() + 1);
247     } break;
248     case MD_BLOCK_OL: {
249         if (m_needsInsertList) // list nested in an empty list
250             m_listStack.push(m_cursor->insertList(m_listFormat));
251         else
252             m_needsInsertList = true;
253         MD_BLOCK_OL_DETAIL *detail = static_cast<MD_BLOCK_OL_DETAIL *>(det);
254         m_listFormat = QTextListFormat();
255         m_listFormat.setIndent(m_listStack.count() + 1);
256         m_listFormat.setNumberSuffix(QChar::fromLatin1(detail->mark_delimiter));
257         m_listFormat.setStyle(QTextListFormat::ListDecimal);
258         qCDebug(lcMD, "OL xx%d level %d", detail->mark_delimiter, m_listStack.count() + 1);
259     } break;
260     case MD_BLOCK_TD: {
261         MD_BLOCK_TD_DETAIL *detail = static_cast<MD_BLOCK_TD_DETAIL *>(det);
262         ++m_tableCol;
263         // absolute movement (and storage of m_tableCol) shouldn't be necessary, but
264         // movePosition(QTextCursor::NextCell) doesn't work
265         QTextTableCell cell = m_currentTable->cellAt(m_tableRowCount - 1, m_tableCol);
266         if (!cell.isValid()) {
267             qWarning("malformed table in Markdown input");
268             return 1;
269         }
270         *m_cursor = cell.firstCursorPosition();
271         QTextBlockFormat blockFmt = m_cursor->blockFormat();
272         blockFmt.setAlignment(MdAlignment(detail->align));
273         m_cursor->setBlockFormat(blockFmt);
274         qCDebug(lcMD) << "TD; align" << detail->align << MdAlignment(detail->align) << "col" << m_tableCol;
275     } break;
276     case MD_BLOCK_TH: {
277         ++m_tableColumnCount;
278         ++m_tableCol;
279         if (m_currentTable->columns() < m_tableColumnCount)
280             m_currentTable->appendColumns(1);
281         auto cell = m_currentTable->cellAt(m_tableRowCount - 1, m_tableCol);
282         if (!cell.isValid()) {
283             qWarning("malformed table in Markdown input");
284             return 1;
285         }
286         auto fmt = cell.format();
287         fmt.setFontWeight(QFont::Bold);
288         cell.setFormat(fmt);
289     } break;
290     case MD_BLOCK_TR: {
291         ++m_tableRowCount;
292         m_nonEmptyTableCells.clear();
293         if (m_currentTable->rows() < m_tableRowCount)
294             m_currentTable->appendRows(1);
295         m_tableCol = -1;
296         qCDebug(lcMD) << "TR" << m_currentTable->rows();
297     } break;
298     case MD_BLOCK_TABLE:
299         m_tableColumnCount = 0;
300         m_tableRowCount = 0;
301         m_currentTable = m_cursor->insertTable(1, 1); // we don't know the dimensions yet
302         break;
303     case MD_BLOCK_HR: {
304         qCDebug(lcMD, "HR");
305         QTextBlockFormat blockFmt;
306         blockFmt.setProperty(QTextFormat::BlockTrailingHorizontalRulerWidth, 1);
307         m_cursor->insertBlock(blockFmt, QTextCharFormat());
308     } break;
309     default:
310         break; // nothing to do for now
311     }
312     return 0; // no error
313 }
314 
cbLeaveBlock(int blockType,void * detail)315 int QTextMarkdownImporter::cbLeaveBlock(int blockType, void *detail)
316 {
317     Q_UNUSED(detail)
318     switch (blockType) {
319     case MD_BLOCK_P:
320         m_listItem = false;
321         break;
322     case MD_BLOCK_UL:
323     case MD_BLOCK_OL:
324         if (Q_UNLIKELY(m_needsInsertList))
325             m_listStack.push(m_cursor->createList(m_listFormat));
326         if (Q_UNLIKELY(m_listStack.isEmpty())) {
327             qCWarning(lcMD, "list ended unexpectedly");
328         } else {
329             qCDebug(lcMD, "list at level %d ended", m_listStack.count());
330             m_listStack.pop();
331         }
332         break;
333     case MD_BLOCK_TR: {
334         // https://github.com/mity/md4c/issues/29
335         // MD4C doesn't tell us explicitly which cells are merged, so merge empty cells
336         // with previous non-empty ones
337         int mergeEnd = -1;
338         int mergeBegin = -1;
339         for (int col = m_tableCol; col >= 0; --col) {
340             if (m_nonEmptyTableCells.contains(col)) {
341                 if (mergeEnd >= 0 && mergeBegin >= 0) {
342                     qCDebug(lcMD) << "merging cells" << mergeBegin << "to" << mergeEnd << "inclusive, on row" << m_currentTable->rows() - 1;
343                     m_currentTable->mergeCells(m_currentTable->rows() - 1, mergeBegin - 1, 1, mergeEnd - mergeBegin + 2);
344                 }
345                 mergeEnd = -1;
346                 mergeBegin = -1;
347             } else {
348                 if (mergeEnd < 0)
349                     mergeEnd = col;
350                 else
351                     mergeBegin = col;
352             }
353         }
354     } break;
355     case MD_BLOCK_QUOTE: {
356         qCDebug(lcMD, "QUOTE level %d ended", m_blockQuoteDepth);
357         --m_blockQuoteDepth;
358         m_needsInsertBlock = true;
359     } break;
360     case MD_BLOCK_TABLE:
361         qCDebug(lcMD) << "table ended with" << m_currentTable->columns() << "cols and" << m_currentTable->rows() << "rows";
362         m_currentTable = nullptr;
363         m_cursor->movePosition(QTextCursor::End);
364         break;
365     case MD_BLOCK_LI:
366         qCDebug(lcMD, "LI at level %d ended", m_listStack.count());
367         m_listItem = false;
368         break;
369     case MD_BLOCK_CODE: {
370         m_codeBlock = false;
371         m_blockCodeLanguage.clear();
372         m_blockCodeFence = 0;
373         if (m_blockQuoteDepth)
374             qCDebug(lcMD, "CODE ended inside QUOTE %d", m_blockQuoteDepth);
375         else
376             qCDebug(lcMD, "CODE ended");
377         m_needsInsertBlock = true;
378     } break;
379     case MD_BLOCK_H:
380         m_cursor->setCharFormat(QTextCharFormat());
381         break;
382     default:
383         break;
384     }
385     return 0; // no error
386 }
387 
cbEnterSpan(int spanType,void * det)388 int QTextMarkdownImporter::cbEnterSpan(int spanType, void *det)
389 {
390     QTextCharFormat charFmt;
391     if (!m_spanFormatStack.isEmpty())
392         charFmt = m_spanFormatStack.top();
393     switch (spanType) {
394     case MD_SPAN_EM:
395         charFmt.setFontItalic(true);
396         break;
397     case MD_SPAN_STRONG:
398         charFmt.setFontWeight(QFont::Bold);
399         break;
400     case MD_SPAN_A: {
401         MD_SPAN_A_DETAIL *detail = static_cast<MD_SPAN_A_DETAIL *>(det);
402         QString url = QString::fromUtf8(detail->href.text, int(detail->href.size));
403         QString title = QString::fromUtf8(detail->title.text, int(detail->title.size));
404         charFmt.setAnchor(true);
405         charFmt.setAnchorHref(url);
406         if (!title.isEmpty())
407             charFmt.setToolTip(title);
408         charFmt.setForeground(m_palette.link());
409         qCDebug(lcMD) << "anchor" << url << title;
410         } break;
411     case MD_SPAN_IMG: {
412         m_imageSpan = true;
413         m_imageFormat = QTextImageFormat();
414         MD_SPAN_IMG_DETAIL *detail = static_cast<MD_SPAN_IMG_DETAIL *>(det);
415         m_imageFormat.setName(QString::fromUtf8(detail->src.text, int(detail->src.size)));
416         m_imageFormat.setProperty(QTextFormat::ImageTitle, QString::fromUtf8(detail->title.text, int(detail->title.size)));
417         break;
418     }
419     case MD_SPAN_CODE:
420         charFmt.setFont(m_monoFont);
421         break;
422     case MD_SPAN_DEL:
423         charFmt.setFontStrikeOut(true);
424         break;
425     }
426     m_spanFormatStack.push(charFmt);
427     qCDebug(lcMD) << spanType << "setCharFormat" << charFmt.font().family() << charFmt.fontWeight()
428                   << (charFmt.fontItalic() ? "italic" : "") << charFmt.foreground().color().name();
429     m_cursor->setCharFormat(charFmt);
430     return 0; // no error
431 }
432 
cbLeaveSpan(int spanType,void * detail)433 int QTextMarkdownImporter::cbLeaveSpan(int spanType, void *detail)
434 {
435     Q_UNUSED(detail)
436     QTextCharFormat charFmt;
437     if (!m_spanFormatStack.isEmpty()) {
438         m_spanFormatStack.pop();
439         if (!m_spanFormatStack.isEmpty())
440             charFmt = m_spanFormatStack.top();
441     }
442     m_cursor->setCharFormat(charFmt);
443     qCDebug(lcMD) << spanType << "setCharFormat" << charFmt.font().family() << charFmt.fontWeight()
444                   << (charFmt.fontItalic() ? "italic" : "") << charFmt.foreground().color().name();
445     if (spanType == int(MD_SPAN_IMG))
446         m_imageSpan = false;
447     return 0; // no error
448 }
449 
cbText(int textType,const char * text,unsigned size)450 int QTextMarkdownImporter::cbText(int textType, const char *text, unsigned size)
451 {
452     if (m_needsInsertBlock)
453         insertBlock();
454 #if QT_CONFIG(regularexpression)
455     static const QRegularExpression openingBracket(QStringLiteral("<[a-zA-Z]"));
456     static const QRegularExpression closingBracket(QStringLiteral("(/>|</)"));
457 #endif
458     QString s = QString::fromUtf8(text, int(size));
459 
460     switch (textType) {
461     case MD_TEXT_NORMAL:
462 #if QT_CONFIG(regularexpression)
463         if (m_htmlTagDepth) {
464             m_htmlAccumulator += s;
465             s = QString();
466         }
467 #endif
468         break;
469     case MD_TEXT_NULLCHAR:
470         s = QString(QChar(0xFFFD)); // CommonMark-required replacement for null
471         break;
472     case MD_TEXT_BR:
473         s = QString(Newline);
474         break;
475     case MD_TEXT_SOFTBR:
476         s = QString(Space);
477         break;
478     case MD_TEXT_CODE:
479         // We'll see MD_SPAN_CODE too, which will set the char format, and that's enough.
480         break;
481 #if QT_CONFIG(texthtmlparser)
482     case MD_TEXT_ENTITY:
483         m_cursor->insertHtml(s);
484         s = QString();
485         break;
486 #endif
487     case MD_TEXT_HTML:
488         // count how many tags are opened and how many are closed
489 #if QT_CONFIG(regularexpression) && QT_CONFIG(texthtmlparser)
490         {
491             int startIdx = 0;
492             while ((startIdx = s.indexOf(openingBracket, startIdx)) >= 0) {
493                 ++m_htmlTagDepth;
494                 startIdx += 2;
495             }
496             startIdx = 0;
497             while ((startIdx = s.indexOf(closingBracket, startIdx)) >= 0) {
498                 --m_htmlTagDepth;
499                 startIdx += 2;
500             }
501         }
502         m_htmlAccumulator += s;
503         if (!m_htmlTagDepth) { // all open tags are now closed
504             qCDebug(lcMD) << "HTML" << m_htmlAccumulator;
505             m_cursor->insertHtml(m_htmlAccumulator);
506             if (m_spanFormatStack.isEmpty())
507                 m_cursor->setCharFormat(QTextCharFormat());
508             else
509                 m_cursor->setCharFormat(m_spanFormatStack.top());
510             m_htmlAccumulator = QString();
511         }
512 #endif
513         s = QString();
514         break;
515     }
516 
517     switch (m_blockType) {
518     case MD_BLOCK_TD:
519         m_nonEmptyTableCells.append(m_tableCol);
520         break;
521     default:
522         break;
523     }
524 
525     if (m_imageSpan) {
526         // TODO we don't yet support alt text with formatting, because of the cases where m_cursor
527         // already inserted the text above.  Rather need to accumulate it in case we need it here.
528         m_imageFormat.setProperty(QTextFormat::ImageAltText, s);
529         qCDebug(lcMD) << "image" << m_imageFormat.name()
530                       << "title" << m_imageFormat.stringProperty(QTextFormat::ImageTitle)
531                       << "alt" << s << "relative to" << m_doc->baseUrl();
532         m_cursor->insertImage(m_imageFormat);
533         return 0; // no error
534     }
535 
536     if (!s.isEmpty())
537         m_cursor->insertText(s);
538     if (m_cursor->currentList()) {
539         // The list item will indent the list item's text, so we don't need indentation on the block.
540         QTextBlockFormat bfmt = m_cursor->blockFormat();
541         bfmt.setIndent(0);
542         m_cursor->setBlockFormat(bfmt);
543     }
544     if (lcMD().isEnabled(QtDebugMsg)) {
545         QTextBlockFormat bfmt = m_cursor->blockFormat();
546         QString debugInfo;
547         if (m_cursor->currentList())
548             debugInfo = QLatin1String("in list at depth ") + QString::number(m_cursor->currentList()->format().indent());
549         if (bfmt.hasProperty(QTextFormat::BlockQuoteLevel))
550             debugInfo += QLatin1String("in blockquote at depth ") +
551                     QString::number(bfmt.intProperty(QTextFormat::BlockQuoteLevel));
552         if (bfmt.hasProperty(QTextFormat::BlockCodeLanguage))
553             debugInfo += QLatin1String("in a code block");
554         qCDebug(lcMD) << textType << "in block" << m_blockType << s << qPrintable(debugInfo)
555                       << "bindent" << bfmt.indent() << "tindent" << bfmt.textIndent()
556                       << "margins" << bfmt.leftMargin() << bfmt.topMargin() << bfmt.bottomMargin() << bfmt.rightMargin();
557     }
558     qCDebug(lcMD) << textType << "in block" << m_blockType << s << "in list?" << m_cursor->currentList()
559                   << "indent" << m_cursor->blockFormat().indent();
560     return 0; // no error
561 }
562 
563 /*!
564     Insert a new block based on stored state.
565 
566     m_cursor cannot store the state for the _next_ block ahead of time, because
567     m_cursor->setBlockFormat() controls the format of the block that the cursor
568     is already in; so cbLeaveBlock() cannot call setBlockFormat() without
569     altering the block that was just added. Therefore cbLeaveBlock() and the
570     following cbEnterBlock() set variables to remember what formatting should
571     come next, and insertBlock() is called just before the actual text
572     insertion, to create a new block with the right formatting.
573 */
insertBlock()574 void QTextMarkdownImporter::insertBlock()
575 {
576     QTextCharFormat charFormat;
577     if (!m_spanFormatStack.isEmpty())
578         charFormat = m_spanFormatStack.top();
579     QTextBlockFormat blockFormat;
580     if (!m_listStack.isEmpty() && !m_needsInsertList && m_listItem) {
581         QTextList *list = m_listStack.top();
582         if (list)
583             blockFormat = list->item(list->count() - 1).blockFormat();
584         else
585             qWarning() << "attempted to insert into a list that no longer exists";
586     }
587     if (m_blockQuoteDepth) {
588         blockFormat.setProperty(QTextFormat::BlockQuoteLevel, m_blockQuoteDepth);
589         blockFormat.setLeftMargin(BlockQuoteIndent * m_blockQuoteDepth);
590         blockFormat.setRightMargin(BlockQuoteIndent);
591     }
592     if (m_codeBlock) {
593         blockFormat.setProperty(QTextFormat::BlockCodeLanguage, m_blockCodeLanguage);
594         if (m_blockCodeFence)
595             blockFormat.setProperty(QTextFormat::BlockCodeFence, QString(QLatin1Char(m_blockCodeFence)));
596         charFormat.setFont(m_monoFont);
597     } else {
598         blockFormat.setTopMargin(m_paragraphMargin);
599         blockFormat.setBottomMargin(m_paragraphMargin);
600     }
601     if (m_markerType == QTextBlockFormat::MarkerType::NoMarker)
602         blockFormat.clearProperty(QTextFormat::BlockMarker);
603     else
604         blockFormat.setMarker(m_markerType);
605     if (!m_listStack.isEmpty())
606         blockFormat.setIndent(m_listStack.count());
607     if (m_doc->isEmpty()) {
608         m_cursor->setBlockFormat(blockFormat);
609         m_cursor->setCharFormat(charFormat);
610     } else {
611         m_cursor->insertBlock(blockFormat, charFormat);
612     }
613     if (m_needsInsertList) {
614         m_listStack.push(m_cursor->createList(m_listFormat));
615     } else if (!m_listStack.isEmpty() && m_listItem && m_listStack.top()) {
616         m_listStack.top()->add(m_cursor->block());
617     }
618     m_needsInsertList = false;
619     m_needsInsertBlock = false;
620 }
621 
622 QT_END_NAMESPACE
623