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
53QT_BEGIN_NAMESPACE
54
55Q_LOGGING_CATEGORY(lcMD, "qt.text.markdown")
56
57static const QChar Newline = QLatin1Char('\n');
58static const QChar Space = QLatin1Char(' ');
59
60// TODO maybe eliminate the margins after all views recognize BlockQuoteLevel, CSS can format it, etc.
61static const int BlockQuoteIndent = 40; // pixels, same as in QTextHtmlParserNode::initializeProperties
62
63static_assert(int(QTextMarkdownImporter::FeatureCollapseWhitespace) == MD_FLAG_COLLAPSEWHITESPACE);
64static_assert(int(QTextMarkdownImporter::FeaturePermissiveATXHeaders) == MD_FLAG_PERMISSIVEATXHEADERS);
65static_assert(int(QTextMarkdownImporter::FeaturePermissiveURLAutoLinks) == MD_FLAG_PERMISSIVEURLAUTOLINKS);
66static_assert(int(QTextMarkdownImporter::FeaturePermissiveMailAutoLinks) == MD_FLAG_PERMISSIVEEMAILAUTOLINKS);
67static_assert(int(QTextMarkdownImporter::FeatureNoIndentedCodeBlocks) == MD_FLAG_NOINDENTEDCODEBLOCKS);
68static_assert(int(QTextMarkdownImporter::FeatureNoHTMLBlocks) == MD_FLAG_NOHTMLBLOCKS);
69static_assert(int(QTextMarkdownImporter::FeatureNoHTMLSpans) == MD_FLAG_NOHTMLSPANS);
70static_assert(int(QTextMarkdownImporter::FeatureTables) == MD_FLAG_TABLES);
71static_assert(int(QTextMarkdownImporter::FeatureStrikeThrough) == MD_FLAG_STRIKETHROUGH);
72static_assert(int(QTextMarkdownImporter::FeaturePermissiveWWWAutoLinks) == MD_FLAG_PERMISSIVEWWWAUTOLINKS);
73static_assert(int(QTextMarkdownImporter::FeaturePermissiveAutoLinks) == MD_FLAG_PERMISSIVEAUTOLINKS);
74static_assert(int(QTextMarkdownImporter::FeatureTasklists) == MD_FLAG_TASKLISTS);
75static_assert(int(QTextMarkdownImporter::FeatureNoHTML) == MD_FLAG_NOHTML);
76static_assert(int(QTextMarkdownImporter::DialectCommonMark) == MD_DIALECT_COMMONMARK);
77static_assert(int(QTextMarkdownImporter::DialectGitHub) == MD_DIALECT_GITHUB);
78
79// --------------------------------------------------------
80// MD4C callback function wrappers
81
82static 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
88static 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
94static 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
100static 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
106static 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
112static 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
121static 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
135QTextMarkdownImporter::QTextMarkdownImporter(QTextMarkdownImporter::Features features)
136 : m_monoFont(QFontDatabase::systemFont(QFontDatabase::FixedFont))
137 , m_features(features)
138{
139}
140
141QTextMarkdownImporter::QTextMarkdownImporter(QTextDocument::MarkdownFeatures features)
142 : QTextMarkdownImporter(static_cast<QTextMarkdownImporter::Features>(int(features)))
143{
144}
145
146void 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
174int 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", int(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, int(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, int(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
315int 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", int(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", int(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
388int 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
433int 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
450int 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(u'\xFFFD')); // 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*/
574void 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
622QT_END_NAMESPACE
623