7#include <QLoggingCategory>
8#if QT_CONFIG(regularexpression)
9#include <QRegularExpression>
12#include <QTextDocument>
13#include <QTextDocumentFragment>
16#if QT_CONFIG(system_textmarkdownreader)
19#include "../../3rdparty/md4c/md4c.h"
24using namespace Qt::StringLiterals;
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));
59static int CbEnterBlock(MD_BLOCKTYPE type,
void *detail,
void *userdata)
61 QTextMarkdownImporter *mdi =
static_cast<QTextMarkdownImporter *>(userdata);
62 return mdi->cbEnterBlock(
int(type), detail);
65static int CbLeaveBlock(MD_BLOCKTYPE type,
void *detail,
void *userdata)
67 QTextMarkdownImporter *mdi =
static_cast<QTextMarkdownImporter *>(userdata);
68 return mdi->cbLeaveBlock(
int(type), detail);
71static int CbEnterSpan(MD_SPANTYPE type,
void *detail,
void *userdata)
73 QTextMarkdownImporter *mdi =
static_cast<QTextMarkdownImporter *>(userdata);
74 return mdi->cbEnterSpan(
int(type), detail);
77static int CbLeaveSpan(MD_SPANTYPE type,
void *detail,
void *userdata)
79 QTextMarkdownImporter *mdi =
static_cast<QTextMarkdownImporter *>(userdata);
80 return mdi->cbLeaveSpan(
int(type), detail);
83static int CbText(MD_TEXTTYPE type,
const MD_CHAR *text, MD_SIZE size,
void *userdata)
85 QTextMarkdownImporter *mdi =
static_cast<QTextMarkdownImporter *>(userdata);
86 return mdi->cbText(
int(type), text, size);
102 return Qt::AlignLeft | Qt::AlignVCenter;
103 case MD_ALIGN_CENTER:
104 return Qt::AlignHCenter | Qt::AlignVCenter;
106 return Qt::AlignRight | Qt::AlignVCenter;
108 return defaultAlignment;
112QTextMarkdownImporter::QTextMarkdownImporter(QTextDocument *doc, QTextMarkdownImporter::Features features)
114 , m_monoFont(QFontDatabase::systemFont(QFontDatabase::FixedFont))
115 , m_features(features)
119QTextMarkdownImporter::QTextMarkdownImporter(QTextDocument *doc, QTextDocument::MarkdownFeatures features)
120 : QTextMarkdownImporter(doc,
static_cast<QTextMarkdownImporter::Features>(
int(features)))
125
126
127
128
129
130
134 QStringView frontMatter, rest;
135 explicit operator
bool()
const noexcept {
return !frontMatter.isEmpty(); }
138 const auto NotFound = R{{}, md};
141
142
143
144
145 QLatin1StringView marker;
153 const auto frontMatterStart = marker.size();
154 const auto endMarkerPos = md.indexOf(marker, frontMatterStart);
156 if (endMarkerPos < 0 || md[endMarkerPos - 1] != QChar::LineFeed)
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())};
165void QTextMarkdownImporter::import(
const QString &markdown)
167 MD_PARSER callbacks = {
169 unsigned(m_features),
178 QTextDocument *doc = m_cursor.document();
179 const auto defaultFont = doc->defaultFont();
180 m_paragraphMargin = defaultFont.pointSize() * 2 / 3;
182 if (defaultFont.pointSize() != -1)
183 m_monoFont.setPointSize(defaultFont.pointSize());
185 m_monoFont.setPixelSize(defaultFont.pixelSize());
186 qCDebug(lcMD) <<
"default font" << defaultFont <<
"mono font" << m_monoFont;
187 QStringView md = markdown;
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();
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();
203int QTextMarkdownImporter::cbEnterBlock(
int blockType,
void *det)
205 m_blockType = blockType;
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()));
212 m_needsInsertBlock =
true;
216 qCDebug(lcMD,
"QUOTE level %d", m_blockQuoteDepth);
218 case MD_BLOCK_CODE: {
219 MD_BLOCK_CODE_DETAIL *detail =
static_cast<MD_BLOCK_CODE_DETAIL *>(det);
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);
228 qCDebug(lcMD,
"CODE lang '%s' info '%s' fenced with '%c'", qPrintable(m_blockCodeLanguage), qPrintable(info), m_blockCodeFence);
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);
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);
243 m_cursor.insertBlock(blockFmt, charFmt);
245 qCDebug(lcMD,
"H%d", detail->level);
248 m_needsInsertBlock =
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";
257 if (m_needsInsertList)
258 m_listStack.push(m_cursor.insertList(m_listFormat));
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) {
266 m_listFormat.setStyle(QTextListFormat::ListCircle);
269 m_listFormat.setStyle(QTextListFormat::ListSquare);
272 m_listFormat.setStyle(QTextListFormat::ListDisc);
275 qCDebug(lcMD,
"UL %c level %d", detail->mark,
int(m_listStack.size()) + 1);
278 if (m_needsInsertList)
279 m_listStack.push(m_cursor.insertList(m_listFormat));
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);
291 MD_BLOCK_TD_DETAIL *detail =
static_cast<MD_BLOCK_TD_DETAIL *>(det);
295 QTextTableCell cell = m_currentTable->cellAt(m_tableRowCount - 1, m_tableCol);
296 if (!cell.isValid()) {
297 qWarning(
"malformed table in Markdown input");
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;
307 ++m_tableColumnCount;
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");
316 auto fmt = cell.format();
317 fmt.setFontWeight(QFont::Bold);
322 m_nonEmptyTableCells.clear();
323 if (m_currentTable->rows() < m_tableRowCount)
324 m_currentTable->appendRows(1);
326 qCDebug(lcMD) <<
"TR" << m_currentTable->rows();
329 m_tableColumnCount = 0;
331 m_currentTable = m_cursor.insertTable(1, 1);
335 QTextBlockFormat blockFmt;
336 blockFmt.setProperty(QTextFormat::BlockTrailingHorizontalRulerWidth, 1);
337 m_cursor.insertBlock(blockFmt, QTextCharFormat());
345int QTextMarkdownImporter::cbLeaveBlock(
int blockType,
void *detail)
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");
359 qCDebug(lcMD,
"list at level %d ended",
int(m_listStack.size()));
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);
385 case MD_BLOCK_QUOTE: {
386 qCDebug(lcMD,
"QUOTE level %d ended", m_blockQuoteDepth);
388 m_needsInsertBlock =
true;
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);
396 qCDebug(lcMD,
"LI at level %d ended",
int(m_listStack.size()));
399 case MD_BLOCK_CODE: {
401 m_blockCodeLanguage.clear();
402 m_blockCodeFence = 0;
403 if (m_blockQuoteDepth)
404 qCDebug(lcMD,
"CODE ended inside QUOTE %d", m_blockQuoteDepth);
406 qCDebug(lcMD,
"CODE ended");
407 m_needsInsertBlock =
true;
410 m_cursor.setCharFormat(QTextCharFormat());
418int QTextMarkdownImporter::cbEnterSpan(
int spanType,
void *det)
420 QTextCharFormat charFmt;
421 if (!m_spanFormatStack.isEmpty())
422 charFmt = m_spanFormatStack.top();
425 charFmt.setFontItalic(
true);
428 charFmt.setFontWeight(QFont::Bold);
431 charFmt.setFontUnderline(
true);
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;
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)));
453 charFmt.setFont(m_monoFont);
454 charFmt.setFontFixedPitch(
true);
457 charFmt.setFontStrikeOut(
true);
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);
468int QTextMarkdownImporter::cbLeaveSpan(
int spanType,
void *detail)
471 QTextCharFormat charFmt;
472 if (!m_spanFormatStack.isEmpty()) {
473 m_spanFormatStack.pop();
474 if (!m_spanFormatStack.isEmpty())
475 charFmt = m_spanFormatStack.top();
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))
486int QTextMarkdownImporter::cbText(
int textType,
const char *text,
unsigned size)
488 if (m_needsInsertBlock)
490#if QT_CONFIG(regularexpression)
491 static const QRegularExpression openingBracket(QStringLiteral(
"<[a-zA-Z]"));
492 static const QRegularExpression closingBracket(QStringLiteral(
"(/>|</)"));
494 QString s = QString::fromUtf8(text,
int(size));
498#if QT_CONFIG(regularexpression)
499 if (m_htmlTagDepth) {
500 m_htmlAccumulator += s;
505 case MD_TEXT_NULLCHAR:
506 s = QString(QChar(u'\xFFFD'));
509 s = QString(qtmi_Newline);
512 s = QString(qtmi_Space);
517#if QT_CONFIG(texthtmlparser)
520 m_htmlAccumulator += s;
522 m_cursor.insertHtml(s);
528#if QT_CONFIG(regularexpression) && QT_CONFIG(texthtmlparser)
531 while ((startIdx = s.indexOf(openingBracket, startIdx)) >= 0) {
536 while ((startIdx = s.indexOf(closingBracket, startIdx)) >= 0) {
541 m_htmlAccumulator += s;
542 if (!m_htmlTagDepth) {
543 qCDebug(lcMD) <<
"HTML" << m_htmlAccumulator;
544 m_cursor.insertHtml(m_htmlAccumulator);
545 if (m_spanFormatStack.isEmpty())
546 m_cursor.setCharFormat(QTextCharFormat());
548 m_cursor.setCharFormat(m_spanFormatStack.top());
549 m_htmlAccumulator = QString();
556 switch (m_blockType) {
558 m_nonEmptyTableCells.append(m_tableCol);
561 if (s == qtmi_Newline) {
564 m_needsInsertBlock =
true;
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);
584 m_cursor.insertText(s);
585 if (m_cursor.currentList()) {
587 QTextBlockFormat bfmt = m_cursor.blockFormat();
589 m_cursor.setBlockFormat(bfmt);
591 if (lcMD().isEnabled(QtDebugMsg)) {
592 QTextBlockFormat bfmt = m_cursor.blockFormat();
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();
609
610
611
612
613
614
615
616
617
618
619void QTextMarkdownImporter::insertBlock()
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();
628 blockFormat = list->item(list->count() - 1).blockFormat();
630 qWarning() <<
"attempted to insert into a list that no longer exists";
632 if (m_blockQuoteDepth) {
633 blockFormat.setProperty(QTextFormat::BlockQuoteLevel, m_blockQuoteDepth);
634 blockFormat.setLeftMargin(qtmi_BlockQuoteIndent * m_blockQuoteDepth);
635 blockFormat.setRightMargin(qtmi_BlockQuoteIndent);
638 blockFormat.setProperty(QTextFormat::BlockCodeLanguage, m_blockCodeLanguage);
639 if (m_blockCodeFence) {
640 blockFormat.setNonBreakableLines(
true);
641 blockFormat.setProperty(QTextFormat::BlockCodeFence, QString(QLatin1Char(m_blockCodeFence)));
643 charFormat.setFont(m_monoFont);
645 blockFormat.setTopMargin(m_paragraphMargin);
646 blockFormat.setBottomMargin(m_paragraphMargin);
648 if (m_markerType == QTextBlockFormat::MarkerType::NoMarker)
649 blockFormat.clearProperty(QTextFormat::BlockMarker);
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);
661 m_cursor.insertBlock(blockFormat, charFormat);
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());
668 m_needsInsertList =
false;
669 m_needsInsertBlock =
false;
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)