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