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