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 ++m_blockQuoteDepth;
213 qCDebug(lcMD, "QUOTE level %d", m_blockQuoteDepth);
214 break;
215 case MD_BLOCK_CODE: {
216 MD_BLOCK_CODE_DETAIL *detail = static_cast<MD_BLOCK_CODE_DETAIL *>(det);
217 m_codeBlock = true;
218 m_blockCodeLanguage = QLatin1StringView(detail->lang.text, int(detail->lang.size));
219 m_blockCodeFence = detail->fence_char;
220 QString info = QLatin1StringView(detail->info.text, int(detail->info.size));
221 m_needsInsertBlock = true;
222 if (m_blockQuoteDepth)
223 qCDebug(lcMD, "CODE lang '%s' info '%s' fenced with '%c' inside QUOTE %d", qPrintable(m_blockCodeLanguage), qPrintable(info), m_blockCodeFence, m_blockQuoteDepth);
224 else
225 qCDebug(lcMD, "CODE lang '%s' info '%s' fenced with '%c'", qPrintable(m_blockCodeLanguage), qPrintable(info), m_blockCodeFence);
226 } break;
227 case MD_BLOCK_H: {
228 MD_BLOCK_H_DETAIL *detail = static_cast<MD_BLOCK_H_DETAIL *>(det);
229 QTextBlockFormat blockFmt;
230 QTextCharFormat charFmt;
231 int sizeAdjustment = 4 - int(detail->level); // H1 to H6: +3 to -2
232 charFmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment);
233 charFmt.setFontWeight(QFont::Bold);
234 blockFmt.setHeadingLevel(int(detail->level));
235 m_needsInsertBlock = false;
236 if (m_cursor.document()->isEmpty()) {
237 m_cursor.setBlockFormat(blockFmt);
238 m_cursor.setCharFormat(charFmt);
239 } else {
240 m_cursor.insertBlock(blockFmt, charFmt);
241 }
242 qCDebug(lcMD, "H%d", detail->level);
243 } break;
244 case MD_BLOCK_LI: {
245 m_needsInsertBlock = true;
246 m_listItem = true;
247 MD_BLOCK_LI_DETAIL *detail = static_cast<MD_BLOCK_LI_DETAIL *>(det);
248 m_markerType = detail->is_task ?
249 (detail->task_mark == ' ' ? QTextBlockFormat::MarkerType::Unchecked : QTextBlockFormat::MarkerType::Checked) :
250 QTextBlockFormat::MarkerType::NoMarker;
251 qCDebug(lcMD) << "LI";
252 } break;
253 case MD_BLOCK_UL: {
254 if (m_needsInsertList) // list nested in an empty list
255 m_listStack.push(m_cursor.insertList(m_listFormat));
256 else
257 m_needsInsertList = true;
258 MD_BLOCK_UL_DETAIL *detail = static_cast<MD_BLOCK_UL_DETAIL *>(det);
259 m_listFormat = QTextListFormat();
260 m_listFormat.setIndent(m_listStack.size() + 1);
261 switch (detail->mark) {
262 case '*':
263 m_listFormat.setStyle(QTextListFormat::ListCircle);
264 break;
265 case '+':
266 m_listFormat.setStyle(QTextListFormat::ListSquare);
267 break;
268 default: // including '-'
269 m_listFormat.setStyle(QTextListFormat::ListDisc);
270 break;
271 }
272 qCDebug(lcMD, "UL %c level %d", detail->mark, int(m_listStack.size()) + 1);
273 } break;
274 case MD_BLOCK_OL: {
275 if (m_needsInsertList) // list nested in an empty list
276 m_listStack.push(m_cursor.insertList(m_listFormat));
277 else
278 m_needsInsertList = true;
279 MD_BLOCK_OL_DETAIL *detail = static_cast<MD_BLOCK_OL_DETAIL *>(det);
280 m_listFormat = QTextListFormat();
281 m_listFormat.setIndent(m_listStack.size() + 1);
282 m_listFormat.setNumberSuffix(QChar::fromLatin1(detail->mark_delimiter));
283 m_listFormat.setStyle(QTextListFormat::ListDecimal);
284 m_listFormat.setStart(detail->start);
285 qCDebug(lcMD, "OL xx%d level %d start %d", detail->mark_delimiter, int(m_listStack.size()) + 1, detail->start);
286 } break;
287 case MD_BLOCK_TD: {
288 MD_BLOCK_TD_DETAIL *detail = static_cast<MD_BLOCK_TD_DETAIL *>(det);
289 ++m_tableCol;
290 // absolute movement (and storage of m_tableCol) shouldn't be necessary, but
291 // movePosition(QTextCursor::NextCell) doesn't work
292 QTextTableCell cell = m_currentTable->cellAt(m_tableRowCount - 1, m_tableCol);
293 if (!cell.isValid()) {
294 qWarning("malformed table in Markdown input");
295 return 1;
296 }
297 m_cursor = cell.firstCursorPosition();
298 QTextBlockFormat blockFmt = m_cursor.blockFormat();
299 blockFmt.setAlignment(MdAlignment(detail->align));
300 m_cursor.setBlockFormat(blockFmt);
301 qCDebug(lcMD) << "TD; align" << detail->align << MdAlignment(detail->align) << "col" << m_tableCol;
302 } break;
303 case MD_BLOCK_TH: {
304 ++m_tableColumnCount;
305 ++m_tableCol;
306 if (m_currentTable->columns() < m_tableColumnCount)
307 m_currentTable->appendColumns(1);
308 auto cell = m_currentTable->cellAt(m_tableRowCount - 1, m_tableCol);
309 if (!cell.isValid()) {
310 qWarning("malformed table in Markdown input");
311 return 1;
312 }
313 auto fmt = cell.format();
314 fmt.setFontWeight(QFont::Bold);
315 cell.setFormat(fmt);
316 } break;
317 case MD_BLOCK_TR: {
318 ++m_tableRowCount;
319 m_nonEmptyTableCells.clear();
320 if (m_currentTable->rows() < m_tableRowCount)
321 m_currentTable->appendRows(1);
322 m_tableCol = -1;
323 qCDebug(lcMD) << "TR" << m_currentTable->rows();
324 } break;
325 case MD_BLOCK_TABLE:
326 m_tableColumnCount = 0;
327 m_tableRowCount = 0;
328 m_currentTable = m_cursor.insertTable(1, 1); // we don't know the dimensions yet
329 break;
330 case MD_BLOCK_HR: {
331 qCDebug(lcMD, "HR");
332 QTextBlockFormat blockFmt;
333 blockFmt.setProperty(QTextFormat::BlockTrailingHorizontalRulerWidth, 1);
334 m_cursor.insertBlock(blockFmt, QTextCharFormat());
335 } break;
336 default:
337 break; // nothing to do for now
338 }
339 return 0; // no error
340}
341
342int QTextMarkdownImporter::cbLeaveBlock(int blockType, void *detail)
343{
344 Q_UNUSED(detail);
345 switch (blockType) {
346 case MD_BLOCK_P:
347 m_listItem = false;
348 break;
349 case MD_BLOCK_UL:
350 case MD_BLOCK_OL:
351 if (Q_UNLIKELY(m_needsInsertList))
352 m_listStack.push(m_cursor.createList(m_listFormat));
353 if (Q_UNLIKELY(m_listStack.isEmpty())) {
354 qCWarning(lcMD, "list ended unexpectedly");
355 } else {
356 qCDebug(lcMD, "list at level %d ended", int(m_listStack.size()));
357 m_listStack.pop();
358 }
359 break;
360 case MD_BLOCK_TR: {
361 // https://github.com/mity/md4c/issues/29
362 // MD4C doesn't tell us explicitly which cells are merged, so merge empty cells
363 // with previous non-empty ones
364 int mergeEnd = -1;
365 int mergeBegin = -1;
366 for (int col = m_tableCol; col >= 0; --col) {
367 if (m_nonEmptyTableCells.contains(col)) {
368 if (mergeEnd >= 0 && mergeBegin >= 0) {
369 qCDebug(lcMD) << "merging cells" << mergeBegin << "to" << mergeEnd << "inclusive, on row" << m_currentTable->rows() - 1;
370 m_currentTable->mergeCells(m_currentTable->rows() - 1, mergeBegin - 1, 1, mergeEnd - mergeBegin + 2);
371 }
372 mergeEnd = -1;
373 mergeBegin = -1;
374 } else {
375 if (mergeEnd < 0)
376 mergeEnd = col;
377 else
378 mergeBegin = col;
379 }
380 }
381 } break;
382 case MD_BLOCK_QUOTE: {
383 qCDebug(lcMD, "QUOTE level %d ended", m_blockQuoteDepth);
384 --m_blockQuoteDepth;
385 m_needsInsertBlock = true;
386 } break;
387 case MD_BLOCK_TABLE:
388 qCDebug(lcMD) << "table ended with" << m_currentTable->columns() << "cols and" << m_currentTable->rows() << "rows";
389 m_currentTable = nullptr;
390 m_cursor.movePosition(QTextCursor::End);
391 break;
392 case MD_BLOCK_LI:
393 qCDebug(lcMD, "LI at level %d ended", int(m_listStack.size()));
394 m_listItem = false;
395 break;
396 case MD_BLOCK_CODE: {
397 m_codeBlock = false;
398 m_blockCodeLanguage.clear();
399 m_blockCodeFence = 0;
400 if (m_blockQuoteDepth)
401 qCDebug(lcMD, "CODE ended inside QUOTE %d", m_blockQuoteDepth);
402 else
403 qCDebug(lcMD, "CODE ended");
404 m_needsInsertBlock = true;
405 } break;
406 case MD_BLOCK_H:
407 m_cursor.setCharFormat(QTextCharFormat());
408 break;
409 default:
410 break;
411 }
412 return 0; // no error
413}
414
415int QTextMarkdownImporter::cbEnterSpan(int spanType, void *det)
416{
417 QTextCharFormat charFmt;
418 if (!m_spanFormatStack.isEmpty())
419 charFmt = m_spanFormatStack.top();
420 switch (spanType) {
421 case MD_SPAN_EM:
422 charFmt.setFontItalic(true);
423 break;
424 case MD_SPAN_STRONG:
425 charFmt.setFontWeight(QFont::Bold);
426 break;
427 case MD_SPAN_U:
428 charFmt.setFontUnderline(true);
429 break;
430 case MD_SPAN_A: {
431 MD_SPAN_A_DETAIL *detail = static_cast<MD_SPAN_A_DETAIL *>(det);
432 QString url = QString::fromUtf8(detail->href.text, int(detail->href.size));
433 QString title = QString::fromUtf8(detail->title.text, int(detail->title.size));
434 charFmt.setAnchor(true);
435 charFmt.setAnchorHref(url);
436 if (!title.isEmpty())
437 charFmt.setToolTip(title);
438 charFmt.setForeground(m_palette.link());
439 qCDebug(lcMD) << "anchor" << url << title;
440 } break;
441 case MD_SPAN_IMG: {
442 m_imageSpan = true;
443 m_imageFormat = QTextImageFormat();
444 MD_SPAN_IMG_DETAIL *detail = static_cast<MD_SPAN_IMG_DETAIL *>(det);
445 m_imageFormat.setName(QString::fromUtf8(detail->src.text, int(detail->src.size)));
446 const QVariant tooltip = QString::fromUtf8(detail->title.text, int(detail->title.size));
447 m_imageFormat.setProperty(QTextFormat::ImageTitle, tooltip);
448 m_imageFormat.setProperty(QTextFormat::TextToolTip, tooltip);
449 break;
450 }
451 case MD_SPAN_CODE:
452 charFmt.setFont(m_monoFont);
453 // Let font size be inherited from the document's default font
454 charFmt.clearProperty(QTextFormat::FontPointSize);
455 charFmt.clearProperty(QTextFormat::FontPixelSize);
456 charFmt.setFontFixedPitch(true);
457 break;
458 case MD_SPAN_DEL:
459 charFmt.setFontStrikeOut(true);
460 break;
461 }
462 m_spanFormatStack.push(charFmt);
463 qCDebug(lcMD) << spanType << "setCharFormat" << charFmt.font().family()
464 << charFmt.fontWeight() << (charFmt.fontItalic() ? "italic" : "")
465 << charFmt.foreground().color().name();
466 m_cursor.setCharFormat(charFmt);
467 return 0; // no error
468}
469
470int QTextMarkdownImporter::cbLeaveSpan(int spanType, void *detail)
471{
472 Q_UNUSED(detail);
473 QTextCharFormat charFmt;
474 if (!m_spanFormatStack.isEmpty()) {
475 m_spanFormatStack.pop();
476 if (!m_spanFormatStack.isEmpty())
477 charFmt = m_spanFormatStack.top();
478 }
479 m_cursor.setCharFormat(charFmt);
480 qCDebug(lcMD) << spanType << "setCharFormat" << charFmt.font().family()
481 << charFmt.fontWeight() << (charFmt.fontItalic() ? "italic" : "")
482 << charFmt.foreground().color().name();
483 if (spanType == int(MD_SPAN_IMG))
484 m_imageSpan = false;
485 return 0; // no error
486}
487
488int QTextMarkdownImporter::cbText(int textType, const char *text, unsigned size)
489{
490 if (m_needsInsertBlock)
491 insertBlock();
492#if QT_CONFIG(regularexpression)
493 static const QRegularExpression openingBracket(QStringLiteral("<[a-zA-Z]"));
494 static const QRegularExpression closingBracket(QStringLiteral("(/>|</)"));
495#endif
496 QString s = QString::fromUtf8(text, int(size));
497
498 switch (textType) {
499 case MD_TEXT_NORMAL:
500#if QT_CONFIG(regularexpression)
501 if (m_htmlTagDepth) {
502 m_htmlAccumulator += s;
503 s = QString();
504 }
505#endif
506 break;
507 case MD_TEXT_NULLCHAR:
508 s = QString(QChar(u'\xFFFD')); // CommonMark-required replacement for null
509 break;
510 case MD_TEXT_BR:
511 s = QString(qtmi_Newline);
512 break;
513 case MD_TEXT_SOFTBR:
514 s = QString(qtmi_Space);
515 break;
516 case MD_TEXT_CODE:
517 // We'll see MD_SPAN_CODE too, which will set the char format, and that's enough.
518 break;
519#if QT_CONFIG(texthtmlparser)
520 case MD_TEXT_ENTITY:
521 if (m_htmlTagDepth)
522 m_htmlAccumulator += s;
523 else
524 m_cursor.insertHtml(s);
525 s = QString();
526 break;
527#endif
528 case MD_TEXT_HTML:
529 // count how many tags are opened and how many are closed
530#if QT_CONFIG(regularexpression) && QT_CONFIG(texthtmlparser)
531 {
532 QRegularExpressionMatchIterator i = openingBracket.globalMatch(s);
533 while (i.hasNext()) {
534 ++m_htmlTagDepth;
535 i.next();
536 }
537 i = closingBracket.globalMatch(s);
538 while (i.hasNext()) {
539 --m_htmlTagDepth;
540 i.next();
541 }
542 }
543 m_htmlAccumulator += s;
544 if (!m_htmlTagDepth) { // all open tags are now closed
545 qCDebug(lcMD) << "HTML" << m_htmlAccumulator;
546 m_cursor.insertHtml(m_htmlAccumulator);
547 if (m_spanFormatStack.isEmpty())
548 m_cursor.setCharFormat(QTextCharFormat());
549 else
550 m_cursor.setCharFormat(m_spanFormatStack.top());
551 m_htmlAccumulator = QString();
552 }
553#endif
554 s = QString();
555 break;
556 }
557
558 switch (m_blockType) {
559 case MD_BLOCK_TD:
560 m_nonEmptyTableCells.append(m_tableCol);
561 break;
562 case MD_BLOCK_CODE:
563 if (s == qtmi_Newline) {
564 // defer a blank line until we see something else in the code block,
565 // to avoid ending every code block with a gratuitous blank line
566 m_needsInsertBlock = true;
567 s = QString();
568 }
569 break;
570 default:
571 break;
572 }
573
574 if (m_imageSpan) {
575 // TODO we don't yet support alt text with formatting, because of the cases where m_cursor
576 // already inserted the text above. Rather need to accumulate it in case we need it here.
577 m_imageFormat.setProperty(QTextFormat::ImageAltText, s);
578 qCDebug(lcMD) << "image" << m_imageFormat.name()
579 << "title" << m_imageFormat.stringProperty(QTextFormat::ImageTitle)
580 << "alt" << s << "relative to" << m_cursor.document()->baseUrl();
581 m_cursor.insertImage(m_imageFormat);
582 return 0; // no error
583 }
584
585 if (!s.isEmpty())
586 m_cursor.insertText(s);
587 if (m_cursor.currentList()) {
588 // The list item will indent the list item's text, so we don't need indentation on the block.
589 QTextBlockFormat bfmt = m_cursor.blockFormat();
590 bfmt.setIndent(0);
591 m_cursor.setBlockFormat(bfmt);
592 }
593 if (lcMD().isEnabled(QtDebugMsg)) {
594 QTextBlockFormat bfmt = m_cursor.blockFormat();
595 QString debugInfo;
596 if (m_cursor.currentList())
597 debugInfo = "in list at depth "_L1 + QString::number(m_cursor.currentList()->format().indent());
598 if (bfmt.hasProperty(QTextFormat::BlockQuoteLevel))
599 debugInfo += "in blockquote at depth "_L1 +
600 QString::number(bfmt.intProperty(QTextFormat::BlockQuoteLevel));
601 if (bfmt.hasProperty(QTextFormat::BlockCodeLanguage))
602 debugInfo += "in a code block"_L1;
603 qCDebug(lcMD) << textType << "in block" << m_blockType << s << qPrintable(debugInfo)
604 << "bindent" << bfmt.indent() << "tindent" << bfmt.textIndent()
605 << "margins" << bfmt.leftMargin() << bfmt.topMargin() << bfmt.bottomMargin() << bfmt.rightMargin();
606 }
607 return 0; // no error
608}
609
610/*!
611 Insert a new block based on stored state.
612
613 m_cursor cannot store the state for the _next_ block ahead of time, because
614 m_cursor.setBlockFormat() controls the format of the block that the cursor
615 is already in; so cbLeaveBlock() cannot call setBlockFormat() without
616 altering the block that was just added. Therefore cbLeaveBlock() and the
617 following cbEnterBlock() set variables to remember what formatting should
618 come next, and insertBlock() is called just before the actual text
619 insertion, to create a new block with the right formatting.
620*/
621void QTextMarkdownImporter::insertBlock()
622{
623 QTextCharFormat charFormat;
624 if (!m_spanFormatStack.isEmpty())
625 charFormat = m_spanFormatStack.top();
626 QTextBlockFormat blockFormat;
627 if (!m_listStack.isEmpty() && !m_needsInsertList && m_listItem) {
628 QTextList *list = m_listStack.top();
629 if (list)
630 blockFormat = list->item(list->count() - 1).blockFormat();
631 else
632 qWarning() << "attempted to insert into a list that no longer exists";
633 }
634 if (m_blockQuoteDepth) {
635 blockFormat.setProperty(QTextFormat::BlockQuoteLevel, m_blockQuoteDepth);
636 blockFormat.setLeftMargin(qtmi_BlockQuoteIndent * m_blockQuoteDepth);
637 blockFormat.setRightMargin(qtmi_BlockQuoteIndent);
638 }
639 if (m_codeBlock) {
640 blockFormat.setProperty(QTextFormat::BlockCodeLanguage, m_blockCodeLanguage);
641 if (m_blockCodeFence) {
642 blockFormat.setNonBreakableLines(true);
643 blockFormat.setProperty(QTextFormat::BlockCodeFence, QString(QLatin1Char(m_blockCodeFence)));
644 }
645 charFormat.setFont(m_monoFont);
646 // Let font size be inherited from the document's default font
647 charFormat.clearProperty(QTextFormat::FontPointSize);
648 charFormat.clearProperty(QTextFormat::FontPixelSize);
649 } else {
650 blockFormat.clearProperty(QTextFormat::BlockCodeLanguage);
651 blockFormat.clearProperty(QTextFormat::BlockCodeFence);
652 blockFormat.setNonBreakableLines(false);
653 blockFormat.setTopMargin(m_paragraphMargin);
654 blockFormat.setBottomMargin(m_paragraphMargin);
655 }
656 if (m_markerType == QTextBlockFormat::MarkerType::NoMarker)
657 blockFormat.clearProperty(QTextFormat::BlockMarker);
658 else
659 blockFormat.setMarker(m_markerType);
660 if (!m_listStack.isEmpty())
661 blockFormat.setIndent(m_listStack.size());
662 if (m_cursor.document()->isEmpty()) {
663 m_cursor.setBlockFormat(blockFormat);
664 m_cursor.setCharFormat(charFormat);
665 } else if (m_listItem) {
666 m_cursor.insertBlock(blockFormat, QTextCharFormat());
667 m_cursor.setCharFormat(charFormat);
668 } else {
669 m_cursor.insertBlock(blockFormat, charFormat);
670 }
671 if (m_needsInsertList) {
672 m_listStack.push(m_cursor.createList(m_listFormat));
673 } else if (!m_listStack.isEmpty() && m_listItem && m_listStack.top()) {
674 m_listStack.top()->add(m_cursor.block());
675 }
676 m_needsInsertList = false;
677 m_needsInsertBlock = false;
678 // Any further blocks in a list item (e.g. a fenced code block) are
679 // continuations, even if it's a tight list and MD4C doesn't emit MD_BLOCK_P.
680 m_listItem = false;
681}
682
683QT_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)