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
63Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeatureCollapseWhitespace) == MD_FLAG_COLLAPSEWHITESPACE);
64Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeaturePermissiveATXHeaders) == MD_FLAG_PERMISSIVEATXHEADERS);
65Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeaturePermissiveURLAutoLinks) == MD_FLAG_PERMISSIVEURLAUTOLINKS);
66Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeaturePermissiveMailAutoLinks) == MD_FLAG_PERMISSIVEEMAILAUTOLINKS);
67Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeatureNoIndentedCodeBlocks) == MD_FLAG_NOINDENTEDCODEBLOCKS);
68Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeatureNoHTMLBlocks) == MD_FLAG_NOHTMLBLOCKS);
69Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeatureNoHTMLSpans) == MD_FLAG_NOHTMLSPANS);
70Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeatureTables) == MD_FLAG_TABLES);
71Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeatureStrikeThrough) == MD_FLAG_STRIKETHROUGH);
72Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeaturePermissiveWWWAutoLinks) == MD_FLAG_PERMISSIVEWWWAUTOLINKS);
73Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeaturePermissiveAutoLinks) == MD_FLAG_PERMISSIVEAUTOLINKS);
74Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeatureTasklists) == MD_FLAG_TASKLISTS);
75Q_STATIC_ASSERT(int(QTextMarkdownImporter::FeatureNoHTML) == MD_FLAG_NOHTML);
76Q_STATIC_ASSERT(int(QTextMarkdownImporter::DialectCommonMark) == MD_DIALECT_COMMONMARK);
77Q_STATIC_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(blockType: 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(blockType: 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(spanType: 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(spanType: 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(textType: 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(type: 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 .abi_version: 0, // abi_version
150 .flags: unsigned(m_features),
151 .enter_block: &CbEnterBlock,
152 .leave_block: &CbLeaveBlock,
153 .enter_span: &CbEnterSpan,
154 .leave_span: &CbLeaveSpan,
155 .text: &CbText,
156 .debug_log: &CbDebugLog,
157 .syntax: 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(text: md.constData(), size: MD_SIZE(md.size()), parser: &callbacks, userdata: 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", 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(propertyId: QTextFormat::FontSizeAdjustment, value: 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(format: blockFmt, charFormat: 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(t: m_cursor->insertList(format: 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(t: m_cursor->insertList(format: 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(c: 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(row: m_tableRowCount - 1, col: m_tableCol);
266 if (!cell.isValid()) {
267 qWarning(msg: "malformed table in Markdown input");
268 return 1;
269 }
270 *m_cursor = cell.firstCursorPosition();
271 QTextBlockFormat blockFmt = m_cursor->blockFormat();
272 blockFmt.setAlignment(MdAlignment(a: detail->align));
273 m_cursor->setBlockFormat(blockFmt);
274 qCDebug(lcMD) << "TD; align" << detail->align << MdAlignment(a: 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(count: 1);
281 auto cell = m_currentTable->cellAt(row: m_tableRowCount - 1, col: m_tableCol);
282 if (!cell.isValid()) {
283 qWarning(msg: "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(count: 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(rows: 1, cols: 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(propertyId: QTextFormat::BlockTrailingHorizontalRulerWidth, value: 1);
307 m_cursor->insertBlock(format: blockFmt, charFormat: 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(t: m_cursor->createList(format: 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(t: 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(row: m_currentTable->rows() - 1, col: mergeBegin - 1, numRows: 1, numCols: 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(op: 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
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(str: detail->href.text, size: int(detail->href.size));
403 QString title = QString::fromUtf8(str: detail->title.text, size: 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(str: detail->src.text, size: int(detail->src.size)));
416 m_imageFormat.setProperty(propertyId: QTextFormat::ImageTitle, value: QString::fromUtf8(str: detail->title.text, size: int(detail->title.size)));
417 break;
418 }
419 case MD_SPAN_CODE:
420 charFmt.setFont(m_monoFont);
421 charFmt.setFontFixedPitch(true);
422 break;
423 case MD_SPAN_DEL:
424 charFmt.setFontStrikeOut(true);
425 break;
426 }
427 m_spanFormatStack.push(t: charFmt);
428 qCDebug(lcMD) << spanType << "setCharFormat" << charFmt.font().family() << charFmt.fontWeight()
429 << (charFmt.fontItalic() ? "italic" : "") << charFmt.foreground().color().name();
430 m_cursor->setCharFormat(charFmt);
431 return 0; // no error
432}
433
434int QTextMarkdownImporter::cbLeaveSpan(int spanType, void *detail)
435{
436 Q_UNUSED(detail)
437 QTextCharFormat charFmt;
438 if (!m_spanFormatStack.isEmpty()) {
439 m_spanFormatStack.pop();
440 if (!m_spanFormatStack.isEmpty())
441 charFmt = m_spanFormatStack.top();
442 }
443 m_cursor->setCharFormat(charFmt);
444 qCDebug(lcMD) << spanType << "setCharFormat" << charFmt.font().family() << charFmt.fontWeight()
445 << (charFmt.fontItalic() ? "italic" : "") << charFmt.foreground().color().name();
446 if (spanType == int(MD_SPAN_IMG))
447 m_imageSpan = false;
448 return 0; // no error
449}
450
451int QTextMarkdownImporter::cbText(int textType, const char *text, unsigned size)
452{
453 if (m_needsInsertBlock)
454 insertBlock();
455#if QT_CONFIG(regularexpression)
456 static const QRegularExpression openingBracket(QStringLiteral("<[a-zA-Z]"));
457 static const QRegularExpression closingBracket(QStringLiteral("(/>|</)"));
458#endif
459 QString s = QString::fromUtf8(str: text, size: int(size));
460
461 switch (textType) {
462 case MD_TEXT_NORMAL:
463#if QT_CONFIG(regularexpression)
464 if (m_htmlTagDepth) {
465 m_htmlAccumulator += s;
466 s = QString();
467 }
468#endif
469 break;
470 case MD_TEXT_NULLCHAR:
471 s = QString(QChar(0xFFFD)); // CommonMark-required replacement for null
472 break;
473 case MD_TEXT_BR:
474 s = QString(Newline);
475 break;
476 case MD_TEXT_SOFTBR:
477 s = QString(Space);
478 break;
479 case MD_TEXT_CODE:
480 // We'll see MD_SPAN_CODE too, which will set the char format, and that's enough.
481 break;
482#if QT_CONFIG(texthtmlparser)
483 case MD_TEXT_ENTITY:
484 m_cursor->insertHtml(html: s);
485 s = QString();
486 break;
487#endif
488 case MD_TEXT_HTML:
489 // count how many tags are opened and how many are closed
490#if QT_CONFIG(regularexpression) && QT_CONFIG(texthtmlparser)
491 {
492 int startIdx = 0;
493 while ((startIdx = s.indexOf(re: openingBracket, from: startIdx)) >= 0) {
494 ++m_htmlTagDepth;
495 startIdx += 2;
496 }
497 startIdx = 0;
498 while ((startIdx = s.indexOf(re: closingBracket, from: startIdx)) >= 0) {
499 --m_htmlTagDepth;
500 startIdx += 2;
501 }
502 }
503 m_htmlAccumulator += s;
504 if (!m_htmlTagDepth) { // all open tags are now closed
505 qCDebug(lcMD) << "HTML" << m_htmlAccumulator;
506 m_cursor->insertHtml(html: m_htmlAccumulator);
507 if (m_spanFormatStack.isEmpty())
508 m_cursor->setCharFormat(QTextCharFormat());
509 else
510 m_cursor->setCharFormat(m_spanFormatStack.top());
511 m_htmlAccumulator = QString();
512 }
513#endif
514 s = QString();
515 break;
516 }
517
518 switch (m_blockType) {
519 case MD_BLOCK_TD:
520 m_nonEmptyTableCells.append(t: m_tableCol);
521 break;
522 default:
523 break;
524 }
525
526 if (m_imageSpan) {
527 // TODO we don't yet support alt text with formatting, because of the cases where m_cursor
528 // already inserted the text above. Rather need to accumulate it in case we need it here.
529 m_imageFormat.setProperty(propertyId: QTextFormat::ImageAltText, value: s);
530 qCDebug(lcMD) << "image" << m_imageFormat.name()
531 << "title" << m_imageFormat.stringProperty(propertyId: QTextFormat::ImageTitle)
532 << "alt" << s << "relative to" << m_doc->baseUrl();
533 m_cursor->insertImage(format: m_imageFormat);
534 return 0; // no error
535 }
536
537 if (!s.isEmpty())
538 m_cursor->insertText(text: s);
539 if (m_cursor->currentList()) {
540 // The list item will indent the list item's text, so we don't need indentation on the block.
541 QTextBlockFormat bfmt = m_cursor->blockFormat();
542 bfmt.setIndent(0);
543 m_cursor->setBlockFormat(bfmt);
544 }
545 if (lcMD().isEnabled(type: QtDebugMsg)) {
546 QTextBlockFormat bfmt = m_cursor->blockFormat();
547 QString debugInfo;
548 if (m_cursor->currentList())
549 debugInfo = QLatin1String("in list at depth ") + QString::number(m_cursor->currentList()->format().indent());
550 if (bfmt.hasProperty(propertyId: QTextFormat::BlockQuoteLevel))
551 debugInfo += QLatin1String("in blockquote at depth ") +
552 QString::number(bfmt.intProperty(propertyId: QTextFormat::BlockQuoteLevel));
553 if (bfmt.hasProperty(propertyId: QTextFormat::BlockCodeLanguage))
554 debugInfo += QLatin1String("in a code block");
555 qCDebug(lcMD) << textType << "in block" << m_blockType << s << qPrintable(debugInfo)
556 << "bindent" << bfmt.indent() << "tindent" << bfmt.textIndent()
557 << "margins" << bfmt.leftMargin() << bfmt.topMargin() << bfmt.bottomMargin() << bfmt.rightMargin();
558 }
559 qCDebug(lcMD) << textType << "in block" << m_blockType << s << "in list?" << m_cursor->currentList()
560 << "indent" << m_cursor->blockFormat().indent();
561 return 0; // no error
562}
563
564/*!
565 Insert a new block based on stored state.
566
567 m_cursor cannot store the state for the _next_ block ahead of time, because
568 m_cursor->setBlockFormat() controls the format of the block that the cursor
569 is already in; so cbLeaveBlock() cannot call setBlockFormat() without
570 altering the block that was just added. Therefore cbLeaveBlock() and the
571 following cbEnterBlock() set variables to remember what formatting should
572 come next, and insertBlock() is called just before the actual text
573 insertion, to create a new block with the right formatting.
574*/
575void QTextMarkdownImporter::insertBlock()
576{
577 QTextCharFormat charFormat;
578 if (!m_spanFormatStack.isEmpty())
579 charFormat = m_spanFormatStack.top();
580 QTextBlockFormat blockFormat;
581 if (!m_listStack.isEmpty() && !m_needsInsertList && m_listItem) {
582 QTextList *list = m_listStack.top();
583 if (list)
584 blockFormat = list->item(i: list->count() - 1).blockFormat();
585 else
586 qWarning() << "attempted to insert into a list that no longer exists";
587 }
588 if (m_blockQuoteDepth) {
589 blockFormat.setProperty(propertyId: QTextFormat::BlockQuoteLevel, value: m_blockQuoteDepth);
590 blockFormat.setLeftMargin(BlockQuoteIndent * m_blockQuoteDepth);
591 blockFormat.setRightMargin(BlockQuoteIndent);
592 }
593 if (m_codeBlock) {
594 blockFormat.setProperty(propertyId: QTextFormat::BlockCodeLanguage, value: m_blockCodeLanguage);
595 if (m_blockCodeFence)
596 blockFormat.setProperty(propertyId: QTextFormat::BlockCodeFence, value: QString(QLatin1Char(m_blockCodeFence)));
597 charFormat.setFont(m_monoFont);
598 } else {
599 blockFormat.setTopMargin(m_paragraphMargin);
600 blockFormat.setBottomMargin(m_paragraphMargin);
601 }
602 if (m_markerType == QTextBlockFormat::MarkerType::NoMarker)
603 blockFormat.clearProperty(propertyId: QTextFormat::BlockMarker);
604 else
605 blockFormat.setMarker(m_markerType);
606 if (!m_listStack.isEmpty())
607 blockFormat.setIndent(m_listStack.count());
608 if (m_doc->isEmpty()) {
609 m_cursor->setBlockFormat(blockFormat);
610 m_cursor->setCharFormat(charFormat);
611 } else {
612 m_cursor->insertBlock(format: blockFormat, charFormat);
613 }
614 if (m_needsInsertList) {
615 m_listStack.push(t: m_cursor->createList(format: m_listFormat));
616 } else if (!m_listStack.isEmpty() && m_listItem && m_listStack.top()) {
617 m_listStack.top()->add(block: m_cursor->block());
618 }
619 m_needsInsertList = false;
620 m_needsInsertBlock = false;
621}
622
623QT_END_NAMESPACE
624

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