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);
54static_assert(
int(QTextMarkdownImporter::DialectGitHub) ==
55 (MD_DIALECT_GITHUB | MD_FLAG_UNDERLINE | QTextMarkdownImporter::FeatureFrontMatter));
60static int CbEnterBlock(MD_BLOCKTYPE type,
void *detail,
void *userdata)
62 QTextMarkdownImporter *mdi =
static_cast<QTextMarkdownImporter *>(userdata);
63 return mdi->cbEnterBlock(
int(type), detail);
66static int CbLeaveBlock(MD_BLOCKTYPE type,
void *detail,
void *userdata)
68 QTextMarkdownImporter *mdi =
static_cast<QTextMarkdownImporter *>(userdata);
69 return mdi->cbLeaveBlock(
int(type), detail);
72static int CbEnterSpan(MD_SPANTYPE type,
void *detail,
void *userdata)
74 QTextMarkdownImporter *mdi =
static_cast<QTextMarkdownImporter *>(userdata);
75 return mdi->cbEnterSpan(
int(type), detail);
78static int CbLeaveSpan(MD_SPANTYPE type,
void *detail,
void *userdata)
80 QTextMarkdownImporter *mdi =
static_cast<QTextMarkdownImporter *>(userdata);
81 return mdi->cbLeaveSpan(
int(type), detail);
84static int CbText(MD_TEXTTYPE type,
const MD_CHAR *text, MD_SIZE size,
void *userdata)
86 QTextMarkdownImporter *mdi =
static_cast<QTextMarkdownImporter *>(userdata);
87 return mdi->cbText(
int(type), text, size);
103 return Qt::AlignLeft | Qt::AlignVCenter;
104 case MD_ALIGN_CENTER:
105 return Qt::AlignHCenter | Qt::AlignVCenter;
107 return Qt::AlignRight | Qt::AlignVCenter;
109 return defaultAlignment;
113QTextMarkdownImporter::QTextMarkdownImporter(QTextDocument *doc, QTextMarkdownImporter::Features features)
115 , m_monoFont(QFontDatabase::systemFont(QFontDatabase::FixedFont))
116 , m_features(features)
120QTextMarkdownImporter::QTextMarkdownImporter(QTextDocument *doc, QTextDocument::MarkdownFeatures features)
121 : QTextMarkdownImporter(doc,
static_cast<QTextMarkdownImporter::Features>(
int(features)))
126
127
128
129
130
131
135 QStringView frontMatter, rest;
136 explicit operator
bool()
const noexcept {
return !frontMatter.isEmpty(); }
139 const auto NotFound = R{{}, md};
142
143
144
145
146 QLatin1StringView marker;
154 const auto frontMatterStart = marker.size();
155 const auto endMarkerPos = md.indexOf(marker, frontMatterStart);
157 if (endMarkerPos < 0 || md[endMarkerPos - 1] != QChar::LineFeed)
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())};
166void QTextMarkdownImporter::import(
const QString &markdown)
168 MD_PARSER callbacks = {
170 unsigned(m_features),
179 QTextDocument *doc = m_cursor.document();
180 const auto defaultFont = doc->defaultFont();
181 m_paragraphMargin = defaultFont.pointSize() * 2 / 3;
183 qCDebug(lcMD) <<
"default font" << defaultFont <<
"mono font" << m_monoFont;
184 QStringView md = markdown;
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();
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();
200int QTextMarkdownImporter::cbEnterBlock(
int blockType,
void *det)
202 m_blockType = blockType;
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()));
209 m_needsInsertBlock =
true;
213 qCDebug(lcMD,
"QUOTE level %d", m_blockQuoteDepth);
215 case MD_BLOCK_CODE: {
216 MD_BLOCK_CODE_DETAIL *detail =
static_cast<MD_BLOCK_CODE_DETAIL *>(det);
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);
225 qCDebug(lcMD,
"CODE lang '%s' info '%s' fenced with '%c'", qPrintable(m_blockCodeLanguage), qPrintable(info), m_blockCodeFence);
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);
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);
240 m_cursor.insertBlock(blockFmt, charFmt);
242 qCDebug(lcMD,
"H%d", detail->level);
245 m_needsInsertBlock =
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";
254 if (m_needsInsertList)
255 m_listStack.push(m_cursor.insertList(m_listFormat));
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) {
263 m_listFormat.setStyle(QTextListFormat::ListCircle);
266 m_listFormat.setStyle(QTextListFormat::ListSquare);
269 m_listFormat.setStyle(QTextListFormat::ListDisc);
272 qCDebug(lcMD,
"UL %c level %d", detail->mark,
int(m_listStack.size()) + 1);
275 if (m_needsInsertList)
276 m_listStack.push(m_cursor.insertList(m_listFormat));
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);
288 MD_BLOCK_TD_DETAIL *detail =
static_cast<MD_BLOCK_TD_DETAIL *>(det);
292 QTextTableCell cell = m_currentTable->cellAt(m_tableRowCount - 1, m_tableCol);
293 if (!cell.isValid()) {
294 qWarning(
"malformed table in Markdown input");
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;
304 ++m_tableColumnCount;
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");
313 auto fmt = cell.format();
314 fmt.setFontWeight(QFont::Bold);
319 m_nonEmptyTableCells.clear();
320 if (m_currentTable->rows() < m_tableRowCount)
321 m_currentTable->appendRows(1);
323 qCDebug(lcMD) <<
"TR" << m_currentTable->rows();
326 m_tableColumnCount = 0;
328 m_currentTable = m_cursor.insertTable(1, 1);
332 QTextBlockFormat blockFmt;
333 blockFmt.setProperty(QTextFormat::BlockTrailingHorizontalRulerWidth, 1);
334 m_cursor.insertBlock(blockFmt, QTextCharFormat());
342int QTextMarkdownImporter::cbLeaveBlock(
int blockType,
void *detail)
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");
356 qCDebug(lcMD,
"list at level %d ended",
int(m_listStack.size()));
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);
382 case MD_BLOCK_QUOTE: {
383 qCDebug(lcMD,
"QUOTE level %d ended", m_blockQuoteDepth);
385 m_needsInsertBlock =
true;
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);
393 qCDebug(lcMD,
"LI at level %d ended",
int(m_listStack.size()));
396 case MD_BLOCK_CODE: {
398 m_blockCodeLanguage.clear();
399 m_blockCodeFence = 0;
400 if (m_blockQuoteDepth)
401 qCDebug(lcMD,
"CODE ended inside QUOTE %d", m_blockQuoteDepth);
403 qCDebug(lcMD,
"CODE ended");
404 m_needsInsertBlock =
true;
407 m_cursor.setCharFormat(QTextCharFormat());
415int QTextMarkdownImporter::cbEnterSpan(
int spanType,
void *det)
417 QTextCharFormat charFmt;
418 if (!m_spanFormatStack.isEmpty())
419 charFmt = m_spanFormatStack.top();
422 charFmt.setFontItalic(
true);
425 charFmt.setFontWeight(QFont::Bold);
428 charFmt.setFontUnderline(
true);
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;
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);
452 charFmt.setFont(m_monoFont);
454 charFmt.clearProperty(QTextFormat::FontPointSize);
455 charFmt.clearProperty(QTextFormat::FontPixelSize);
456 charFmt.setFontFixedPitch(
true);
459 charFmt.setFontStrikeOut(
true);
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);
470int QTextMarkdownImporter::cbLeaveSpan(
int spanType,
void *detail)
473 QTextCharFormat charFmt;
474 if (!m_spanFormatStack.isEmpty()) {
475 m_spanFormatStack.pop();
476 if (!m_spanFormatStack.isEmpty())
477 charFmt = m_spanFormatStack.top();
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))
488int QTextMarkdownImporter::cbText(
int textType,
const char *text,
unsigned size)
490 if (m_needsInsertBlock)
492#if QT_CONFIG(regularexpression)
493 static const QRegularExpression openingBracket(QStringLiteral(
"<[a-zA-Z]"));
494 static const QRegularExpression closingBracket(QStringLiteral(
"(/>|</)"));
496 QString s = QString::fromUtf8(text,
int(size));
500#if QT_CONFIG(regularexpression)
501 if (m_htmlTagDepth) {
502 m_htmlAccumulator += s;
507 case MD_TEXT_NULLCHAR:
508 s = QString(QChar(u'\xFFFD'));
511 s = QString(qtmi_Newline);
514 s = QString(qtmi_Space);
519#if QT_CONFIG(texthtmlparser)
522 m_htmlAccumulator += s;
524 m_cursor.insertHtml(s);
530#if QT_CONFIG(regularexpression) && QT_CONFIG(texthtmlparser)
532 QRegularExpressionMatchIterator i = openingBracket.globalMatch(s);
533 while (i.hasNext()) {
537 i = closingBracket.globalMatch(s);
538 while (i.hasNext()) {
543 m_htmlAccumulator += s;
544 if (!m_htmlTagDepth) {
545 qCDebug(lcMD) <<
"HTML" << m_htmlAccumulator;
546 m_cursor.insertHtml(m_htmlAccumulator);
547 if (m_spanFormatStack.isEmpty())
548 m_cursor.setCharFormat(QTextCharFormat());
550 m_cursor.setCharFormat(m_spanFormatStack.top());
551 m_htmlAccumulator = QString();
558 switch (m_blockType) {
560 m_nonEmptyTableCells.append(m_tableCol);
563 if (s == qtmi_Newline) {
566 m_needsInsertBlock =
true;
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);
586 m_cursor.insertText(s);
587 if (m_cursor.currentList()) {
589 QTextBlockFormat bfmt = m_cursor.blockFormat();
591 m_cursor.setBlockFormat(bfmt);
593 if (lcMD().isEnabled(QtDebugMsg)) {
594 QTextBlockFormat bfmt = m_cursor.blockFormat();
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();
611
612
613
614
615
616
617
618
619
620
621void QTextMarkdownImporter::insertBlock()
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();
630 blockFormat = list->item(list->count() - 1).blockFormat();
632 qWarning() <<
"attempted to insert into a list that no longer exists";
634 if (m_blockQuoteDepth) {
635 blockFormat.setProperty(QTextFormat::BlockQuoteLevel, m_blockQuoteDepth);
636 blockFormat.setLeftMargin(qtmi_BlockQuoteIndent * m_blockQuoteDepth);
637 blockFormat.setRightMargin(qtmi_BlockQuoteIndent);
640 blockFormat.setProperty(QTextFormat::BlockCodeLanguage, m_blockCodeLanguage);
641 if (m_blockCodeFence) {
642 blockFormat.setNonBreakableLines(
true);
643 blockFormat.setProperty(QTextFormat::BlockCodeFence, QString(QLatin1Char(m_blockCodeFence)));
645 charFormat.setFont(m_monoFont);
647 charFormat.clearProperty(QTextFormat::FontPointSize);
648 charFormat.clearProperty(QTextFormat::FontPixelSize);
650 blockFormat.clearProperty(QTextFormat::BlockCodeLanguage);
651 blockFormat.clearProperty(QTextFormat::BlockCodeFence);
652 blockFormat.setNonBreakableLines(
false);
653 blockFormat.setTopMargin(m_paragraphMargin);
654 blockFormat.setBottomMargin(m_paragraphMargin);
656 if (m_markerType == QTextBlockFormat::MarkerType::NoMarker)
657 blockFormat.clearProperty(QTextFormat::BlockMarker);
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);
669 m_cursor.insertBlock(blockFormat, charFormat);
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());
676 m_needsInsertList =
false;
677 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)