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