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