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

source code of qtbase/src/gui/text/qtextmarkdownimporter.cpp