7#include <QLoggingCategory>
8#if QT_CONFIG(regularexpression)
9#include <QRegularExpression>
10#include <QRegularExpressionMatchIterator>
13#include <QTextDocument>
14#include <QTextDocumentFragment>
17#if QT_CONFIG(system_textmarkdownreader)
20#include "../../3rdparty/md4c/md4c.h"
25using namespace Qt::StringLiterals;
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);
55static_assert(
int(QTextMarkdownImporter::DialectGitHub) ==
56 (MD_FLAG_PERMISSIVEAUTOLINKS | MD_FLAG_TABLES | MD_FLAG_STRIKETHROUGH | MD_FLAG_TASKLISTS
57 | MD_FLAG_UNDERLINE | QTextMarkdownImporter::FeatureFrontMatter));
62static int CbEnterBlock(MD_BLOCKTYPE type,
void *detail,
void *userdata)
64 QTextMarkdownImporter *mdi =
static_cast<QTextMarkdownImporter *>(userdata);
65 return mdi->cbEnterBlock(
int(type), detail);
68static int CbLeaveBlock(MD_BLOCKTYPE type,
void *detail,
void *userdata)
70 QTextMarkdownImporter *mdi =
static_cast<QTextMarkdownImporter *>(userdata);
71 return mdi->cbLeaveBlock(
int(type), detail);
74static int CbEnterSpan(MD_SPANTYPE type,
void *detail,
void *userdata)
76 QTextMarkdownImporter *mdi =
static_cast<QTextMarkdownImporter *>(userdata);
77 return mdi->cbEnterSpan(
int(type), detail);
80static int CbLeaveSpan(MD_SPANTYPE type,
void *detail,
void *userdata)
82 QTextMarkdownImporter *mdi =
static_cast<QTextMarkdownImporter *>(userdata);
83 return mdi->cbLeaveSpan(
int(type), detail);
86static int CbText(MD_TEXTTYPE type,
const MD_CHAR *text, MD_SIZE size,
void *userdata)
88 QTextMarkdownImporter *mdi =
static_cast<QTextMarkdownImporter *>(userdata);
89 return mdi->cbText(
int(type), text, size);
105 return Qt::AlignLeft | Qt::AlignVCenter;
106 case MD_ALIGN_CENTER:
107 return Qt::AlignHCenter | Qt::AlignVCenter;
109 return Qt::AlignRight | Qt::AlignVCenter;
111 return defaultAlignment;
115QTextMarkdownImporter::QTextMarkdownImporter(QTextDocument *doc, QTextMarkdownImporter::Features features)
117 , m_monoFont(QFontDatabase::systemFont(QFontDatabase::FixedFont))
118 , m_features(features)
122QTextMarkdownImporter::QTextMarkdownImporter(QTextDocument *doc, QTextDocument::MarkdownFeatures features)
123 : QTextMarkdownImporter(doc,
static_cast<QTextMarkdownImporter::Features>(
int(features)))
128
129
130
131
132
133
137 QStringView frontMatter, rest;
138 explicit operator
bool()
const noexcept {
return !frontMatter.isEmpty(); }
141 const auto NotFound = R{{}, md};
144
145
146
147
148 QLatin1StringView marker;
156 const auto frontMatterStart = marker.size();
157 const auto endMarkerPos = md.indexOf(marker, frontMatterStart);
159 if (endMarkerPos < 0 || md[endMarkerPos - 1] != QChar::LineFeed)
162 Q_ASSERT(frontMatterStart < md.size());
163 Q_ASSERT(endMarkerPos < md.size());
164 const auto frontMatter = md.sliced(frontMatterStart, endMarkerPos - frontMatterStart);
165 return R{frontMatter, md.sliced(endMarkerPos + marker.size())};
168void QTextMarkdownImporter::import(
const QString &markdown)
170 MD_PARSER callbacks = {
172 unsigned(m_features) &
unsigned(0xFFEFFFFF),
181 QTextDocument *doc = m_cursor.document();
182 const auto defaultFont = doc->defaultFont();
183 m_paragraphMargin = defaultFont.pointSize() * 2 / 3;
185 qCDebug(lcMD) <<
"default font" << defaultFont <<
"mono font" << m_monoFont;
186 QStringView md = markdown;
188 if (m_features.testFlag(QTextMarkdownImporter::FeatureFrontMatter)) {
189 if (
const auto split = splitFrontMatter(md)) {
190 doc->setMetaInformation(QTextDocument::FrontMatter, split.frontMatter.toString());
191 qCDebug(lcMD) <<
"extracted FrontMatter: size" << split.frontMatter.size();
196 const auto mdUtf8 = md.toUtf8();
197 m_cursor.beginEditBlock();
198 md_parse(mdUtf8.constData(), MD_SIZE(mdUtf8.size()), &callbacks,
this);
199 m_cursor.endEditBlock();
202int QTextMarkdownImporter::cbEnterBlock(
int blockType,
void *det)
204 m_blockType = blockType;
207 if (!m_listStack.isEmpty())
208 qCDebug(lcMD, m_listItem ?
"P of LI at level %d" :
"P continuation inside LI at level %d",
int(m_listStack.size()));
211 m_needsInsertBlock =
true;
217 if (!m_blockQuoteDepth)
218 m_blockQuoteInsideListItem = !m_listStack.isEmpty() || m_needsInsertList;
220 qCDebug(lcMD,
"QUOTE level %d inside list item %d", m_blockQuoteDepth,
221 m_blockQuoteInsideListItem);
223 case MD_BLOCK_CODE: {
224 MD_BLOCK_CODE_DETAIL *detail =
static_cast<MD_BLOCK_CODE_DETAIL *>(det);
226 m_blockCodeLanguage = QLatin1StringView(detail->lang.text,
int(detail->lang.size));
227 m_blockCodeFence = detail->fence_char;
228 QString info = QLatin1StringView(detail->info.text,
int(detail->info.size));
229 m_needsInsertBlock =
true;
230 if (m_blockQuoteDepth)
231 qCDebug(lcMD,
"CODE lang '%s' info '%s' fenced with '%c' inside QUOTE %d", qPrintable(m_blockCodeLanguage), qPrintable(info), m_blockCodeFence, m_blockQuoteDepth);
233 qCDebug(lcMD,
"CODE lang '%s' info '%s' fenced with '%c'", qPrintable(m_blockCodeLanguage), qPrintable(info), m_blockCodeFence);
236 MD_BLOCK_H_DETAIL *detail =
static_cast<MD_BLOCK_H_DETAIL *>(det);
237 QTextBlockFormat blockFmt;
238 QTextCharFormat charFmt;
239 int sizeAdjustment = 4 -
int(detail->level);
240 charFmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment);
241 charFmt.setFontWeight(QFont::Bold);
242 blockFmt.setHeadingLevel(
int(detail->level));
243 m_needsInsertBlock =
false;
244 if (m_cursor.document()->isEmpty()) {
245 m_cursor.setBlockFormat(blockFmt);
246 m_cursor.setCharFormat(charFmt);
248 m_cursor.insertBlock(blockFmt, charFmt);
250 qCDebug(lcMD,
"H%d", detail->level);
253 m_needsInsertBlock =
true;
255 MD_BLOCK_LI_DETAIL *detail =
static_cast<MD_BLOCK_LI_DETAIL *>(det);
256 m_markerType = detail->is_task ?
257 (detail->task_mark ==
' ' ? QTextBlockFormat::MarkerType::Unchecked : QTextBlockFormat::MarkerType::Checked) :
258 QTextBlockFormat::MarkerType::NoMarker;
259 qCDebug(lcMD) <<
"LI";
262 if (m_needsInsertList)
263 m_listStack.push(m_cursor.insertList(m_listFormat));
265 m_needsInsertList =
true;
266 MD_BLOCK_UL_DETAIL *detail =
static_cast<MD_BLOCK_UL_DETAIL *>(det);
267 m_listFormat = QTextListFormat();
268 m_listFormat.setIndent(m_listStack.size() + 1);
269 switch (detail->mark) {
271 m_listFormat.setStyle(QTextListFormat::ListCircle);
274 m_listFormat.setStyle(QTextListFormat::ListSquare);
277 m_listFormat.setStyle(QTextListFormat::ListDisc);
280 qCDebug(lcMD,
"UL %c level %d", detail->mark,
int(m_listStack.size()) + 1);
283 if (m_needsInsertList)
284 m_listStack.push(m_cursor.insertList(m_listFormat));
286 m_needsInsertList =
true;
287 MD_BLOCK_OL_DETAIL *detail =
static_cast<MD_BLOCK_OL_DETAIL *>(det);
288 m_listFormat = QTextListFormat();
289 m_listFormat.setIndent(m_listStack.size() + 1);
290 m_listFormat.setNumberSuffix(QChar::fromLatin1(detail->mark_delimiter));
291 m_listFormat.setStyle(QTextListFormat::ListDecimal);
292 m_listFormat.setStart(detail->start);
293 qCDebug(lcMD,
"OL xx%d level %d start %d", detail->mark_delimiter,
int(m_listStack.size()) + 1, detail->start);
296 MD_BLOCK_TD_DETAIL *detail =
static_cast<MD_BLOCK_TD_DETAIL *>(det);
300 QTextTableCell cell = m_currentTable->cellAt(m_tableRowCount - 1, m_tableCol);
301 if (!cell.isValid()) {
302 qWarning(
"malformed table in Markdown input");
305 m_cursor = cell.firstCursorPosition();
306 QTextBlockFormat blockFmt = m_cursor.blockFormat();
307 blockFmt.setAlignment(MdAlignment(detail->align));
308 m_cursor.setBlockFormat(blockFmt);
309 qCDebug(lcMD) <<
"TD; align" << detail->align << MdAlignment(detail->align) <<
"col" << m_tableCol;
312 ++m_tableColumnCount;
314 if (m_currentTable->columns() < m_tableColumnCount)
315 m_currentTable->appendColumns(1);
316 auto cell = m_currentTable->cellAt(m_tableRowCount - 1, m_tableCol);
317 if (!cell.isValid()) {
318 qWarning(
"malformed table in Markdown input");
321 auto fmt = cell.format();
322 fmt.setFontWeight(QFont::Bold);
327 m_nonEmptyTableCells.clear();
328 if (m_currentTable->rows() < m_tableRowCount)
329 m_currentTable->appendRows(1);
331 qCDebug(lcMD) <<
"TR" << m_currentTable->rows();
334 m_tableColumnCount = 0;
336 m_currentTable = m_cursor.insertTable(1, 1);
340 QTextBlockFormat blockFmt;
341 blockFmt.setProperty(QTextFormat::BlockTrailingHorizontalRulerWidth, 1);
342 m_cursor.insertBlock(blockFmt, QTextCharFormat());
350int QTextMarkdownImporter::cbLeaveBlock(
int blockType,
void *detail)
359 if (Q_UNLIKELY(m_needsInsertList))
360 m_listStack.push(m_cursor.createList(m_listFormat));
361 if (Q_UNLIKELY(m_listStack.isEmpty())) {
362 qCWarning(lcMD,
"list ended unexpectedly");
364 qCDebug(lcMD,
"list at level %d ended",
int(m_listStack.size()));
374 for (
int col = m_tableCol; col >= 0; --col) {
375 if (m_nonEmptyTableCells.contains(col)) {
376 if (mergeEnd >= 0 && mergeBegin >= 0) {
377 qCDebug(lcMD) <<
"merging cells" << mergeBegin <<
"to" << mergeEnd <<
"inclusive, on row" << m_currentTable->rows() - 1;
378 m_currentTable->mergeCells(m_currentTable->rows() - 1, mergeBegin - 1, 1, mergeEnd - mergeBegin + 2);
390 case MD_BLOCK_QUOTE: {
391 qCDebug(lcMD,
"QUOTE level %d ended", m_blockQuoteDepth);
393 if (!m_blockQuoteDepth)
394 m_blockQuoteInsideListItem =
false;
395 m_needsInsertBlock =
true;
398 qCDebug(lcMD) <<
"table ended with" << m_currentTable->columns() <<
"cols and" << m_currentTable->rows() <<
"rows";
399 m_currentTable =
nullptr;
400 m_cursor.movePosition(QTextCursor::End);
403 qCDebug(lcMD,
"LI at level %d ended",
int(m_listStack.size()));
406 case MD_BLOCK_CODE: {
408 m_blockCodeLanguage.clear();
409 m_blockCodeFence = 0;
410 if (m_blockQuoteDepth)
411 qCDebug(lcMD,
"CODE ended inside QUOTE %d", m_blockQuoteDepth);
413 qCDebug(lcMD,
"CODE ended");
414 m_needsInsertBlock =
true;
417 m_cursor.setCharFormat(QTextCharFormat());
425int QTextMarkdownImporter::cbEnterSpan(
int spanType,
void *det)
427 QTextCharFormat charFmt;
428 if (!m_spanFormatStack.isEmpty())
429 charFmt = m_spanFormatStack.top();
432 charFmt.setFontItalic(
true);
435 charFmt.setFontWeight(QFont::Bold);
438 charFmt.setFontUnderline(
true);
441 MD_SPAN_A_DETAIL *detail =
static_cast<MD_SPAN_A_DETAIL *>(det);
442 QString url = QString::fromUtf8(detail->href.text,
int(detail->href.size));
443 QString title = QString::fromUtf8(detail->title.text,
int(detail->title.size));
444 charFmt.setAnchor(
true);
445 charFmt.setAnchorHref(url);
446 if (!title.isEmpty())
447 charFmt.setToolTip(title);
448 charFmt.setForeground(m_palette.link());
449 qCDebug(lcMD) <<
"anchor" << url << title;
453 m_imageFormat = QTextImageFormat();
454 MD_SPAN_IMG_DETAIL *detail =
static_cast<MD_SPAN_IMG_DETAIL *>(det);
455 m_imageFormat.setName(QString::fromUtf8(detail->src.text,
int(detail->src.size)));
456 const QVariant tooltip = QString::fromUtf8(detail->title.text,
int(detail->title.size));
457 m_imageFormat.setProperty(QTextFormat::ImageTitle, tooltip);
458 m_imageFormat.setProperty(QTextFormat::TextToolTip, tooltip);
462 charFmt.setFont(m_monoFont);
464 charFmt.clearProperty(QTextFormat::FontPointSize);
465 charFmt.clearProperty(QTextFormat::FontPixelSize);
466 charFmt.setFontFixedPitch(
true);
469 charFmt.setFontStrikeOut(
true);
472 m_spanFormatStack.push(charFmt);
473 qCDebug(lcMD) << spanType <<
"setCharFormat" << charFmt.font().family()
474 << charFmt.fontWeight() << (charFmt.fontItalic() ?
"italic" :
"")
475 << charFmt.foreground().color().name();
476 m_cursor.setCharFormat(charFmt);
480int QTextMarkdownImporter::cbLeaveSpan(
int spanType,
void *detail)
483 QTextCharFormat charFmt;
484 if (!m_spanFormatStack.isEmpty()) {
485 m_spanFormatStack.pop();
486 if (!m_spanFormatStack.isEmpty())
487 charFmt = m_spanFormatStack.top();
489 m_cursor.setCharFormat(charFmt);
490 qCDebug(lcMD) << spanType <<
"setCharFormat" << charFmt.font().family()
491 << charFmt.fontWeight() << (charFmt.fontItalic() ?
"italic" :
"")
492 << charFmt.foreground().color().name();
493 if (spanType ==
int(MD_SPAN_IMG))
498int QTextMarkdownImporter::cbText(
int textType,
const char *text,
unsigned size)
500 if (m_needsInsertBlock)
502#if QT_CONFIG(regularexpression)
503 static const QRegularExpression openingBracket(QStringLiteral(
"<[a-zA-Z]"));
504 static const QRegularExpression closingBracket(QStringLiteral(
"(/>|</)"));
506 QString s = QString::fromUtf8(text,
int(size));
510#if QT_CONFIG(regularexpression)
511 if (m_htmlTagDepth) {
512 m_htmlAccumulator += s;
517 case MD_TEXT_NULLCHAR:
518 s = QString(QChar(u'\xFFFD'));
521 s = QString(qtmi_Newline);
524 s = QString(qtmi_Space);
529#if QT_CONFIG(texthtmlparser)
532 m_htmlAccumulator += s;
534 m_cursor.insertHtml(s);
540#if QT_CONFIG(regularexpression) && QT_CONFIG(texthtmlparser)
542 QRegularExpressionMatchIterator i = openingBracket.globalMatch(s);
543 while (i.hasNext()) {
547 i = closingBracket.globalMatch(s);
548 while (i.hasNext()) {
553 m_htmlAccumulator += s;
554 if (!m_htmlTagDepth) {
555 qCDebug(lcMD) <<
"HTML" << m_htmlAccumulator;
556 m_cursor.insertHtml(m_htmlAccumulator);
557 if (m_spanFormatStack.isEmpty())
558 m_cursor.setCharFormat(QTextCharFormat());
560 m_cursor.setCharFormat(m_spanFormatStack.top());
561 m_htmlAccumulator = QString();
568 switch (m_blockType) {
570 m_nonEmptyTableCells.append(m_tableCol);
573 if (s == qtmi_Newline) {
576 m_needsInsertBlock =
true;
587 m_imageFormat.setProperty(QTextFormat::ImageAltText, s);
588 qCDebug(lcMD) <<
"image" << m_imageFormat.name()
589 <<
"title" << m_imageFormat.stringProperty(QTextFormat::ImageTitle)
590 <<
"alt" << s <<
"relative to" << m_cursor.document()->baseUrl();
591 m_cursor.insertImage(m_imageFormat);
596 m_cursor.insertText(s);
597 if (m_cursor.currentList()) {
599 QTextBlockFormat bfmt = m_cursor.blockFormat();
601 m_cursor.setBlockFormat(bfmt);
603 if (lcMD().isEnabled(QtDebugMsg)) {
604 QTextBlockFormat bfmt = m_cursor.blockFormat();
606 if (m_cursor.currentList())
607 debugInfo =
"in list at depth "_L1 + QString::number(m_cursor.currentList()->format().indent());
608 if (bfmt.hasProperty(QTextFormat::BlockQuoteLevel))
609 debugInfo +=
"in blockquote at depth "_L1 +
610 QString::number(bfmt.intProperty(QTextFormat::BlockQuoteLevel));
611 if (bfmt.hasProperty(QTextFormat::BlockCodeLanguage))
612 debugInfo +=
"in a code block"_L1;
613 qCDebug(lcMD) << textType <<
"in block" << m_blockType << s << qPrintable(debugInfo)
614 <<
"bindent" << bfmt.indent() <<
"tindent" << bfmt.textIndent()
615 <<
"margins" << bfmt.leftMargin() << bfmt.topMargin() << bfmt.bottomMargin() << bfmt.rightMargin();
621
622
623
624
625
626
627
628
629
630
631void QTextMarkdownImporter::insertBlock()
633 QTextCharFormat charFormat;
634 if (!m_spanFormatStack.isEmpty())
635 charFormat = m_spanFormatStack.top();
636 QTextBlockFormat blockFormat;
637 if (!m_listStack.isEmpty() && !m_needsInsertList && m_listItem) {
638 QTextList *list = m_listStack.top();
640 blockFormat = list->item(list->count() - 1).blockFormat();
642 qWarning() <<
"attempted to insert into a list that no longer exists";
644 if (m_blockQuoteDepth) {
645 blockFormat.setProperty(QTextFormat::BlockQuoteLevel, m_blockQuoteDepth);
646 blockFormat.setLeftMargin(qtmi_BlockQuoteIndent * m_blockQuoteDepth);
647 blockFormat.setRightMargin(qtmi_BlockQuoteIndent);
650 if (m_blockQuoteInsideListItem)
651 blockFormat.setProperty(QTextFormat::BlockQuoteInsideListItem,
true);
654 blockFormat.setProperty(QTextFormat::BlockCodeLanguage, m_blockCodeLanguage);
655 if (m_blockCodeFence) {
656 blockFormat.setNonBreakableLines(
true);
657 blockFormat.setProperty(QTextFormat::BlockCodeFence, QString(QLatin1Char(m_blockCodeFence)));
659 charFormat.setFont(m_monoFont);
661 charFormat.clearProperty(QTextFormat::FontPointSize);
662 charFormat.clearProperty(QTextFormat::FontPixelSize);
664 blockFormat.clearProperty(QTextFormat::BlockCodeLanguage);
665 blockFormat.clearProperty(QTextFormat::BlockCodeFence);
666 blockFormat.setNonBreakableLines(
false);
667 blockFormat.setTopMargin(m_paragraphMargin);
668 blockFormat.setBottomMargin(m_paragraphMargin);
670 if (m_markerType == QTextBlockFormat::MarkerType::NoMarker)
671 blockFormat.clearProperty(QTextFormat::BlockMarker);
673 blockFormat.setMarker(m_markerType);
674 if (!m_listStack.isEmpty())
675 blockFormat.setIndent(m_listStack.size());
676 if (m_cursor.document()->isEmpty()) {
677 m_cursor.setBlockFormat(blockFormat);
678 m_cursor.setCharFormat(charFormat);
679 }
else if (m_listItem) {
680 m_cursor.insertBlock(blockFormat, QTextCharFormat());
681 m_cursor.setCharFormat(charFormat);
683 m_cursor.insertBlock(blockFormat, charFormat);
685 if (m_needsInsertList) {
686 m_listStack.push(m_cursor.createList(m_listFormat));
687 }
else if (!m_listStack.isEmpty() && m_listItem && m_listStack.top()) {
688 m_listStack.top()->add(m_cursor.block());
690 m_needsInsertList =
false;
691 m_needsInsertBlock =
false;
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)