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;
215 if (!m_blockQuoteDepth)
216 m_blockQuoteInsideListItem = !m_listStack.isEmpty() || m_needsInsertList;
218 qCDebug(lcMD,
"QUOTE level %d inside list item %d", m_blockQuoteDepth,
219 m_blockQuoteInsideListItem);
221 case MD_BLOCK_CODE: {
222 MD_BLOCK_CODE_DETAIL *detail =
static_cast<MD_BLOCK_CODE_DETAIL *>(det);
224 m_blockCodeLanguage = QLatin1StringView(detail->lang.text,
int(detail->lang.size));
225 m_blockCodeFence = detail->fence_char;
226 QString info = QLatin1StringView(detail->info.text,
int(detail->info.size));
227 m_needsInsertBlock =
true;
228 if (m_blockQuoteDepth)
229 qCDebug(lcMD,
"CODE lang '%s' info '%s' fenced with '%c' inside QUOTE %d", qPrintable(m_blockCodeLanguage), qPrintable(info), m_blockCodeFence, m_blockQuoteDepth);
231 qCDebug(lcMD,
"CODE lang '%s' info '%s' fenced with '%c'", qPrintable(m_blockCodeLanguage), qPrintable(info), m_blockCodeFence);
234 MD_BLOCK_H_DETAIL *detail =
static_cast<MD_BLOCK_H_DETAIL *>(det);
235 QTextBlockFormat blockFmt;
236 QTextCharFormat charFmt;
237 int sizeAdjustment = 4 -
int(detail->level);
238 charFmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment);
239 charFmt.setFontWeight(QFont::Bold);
240 blockFmt.setHeadingLevel(
int(detail->level));
241 m_needsInsertBlock =
false;
242 if (m_cursor.document()->isEmpty()) {
243 m_cursor.setBlockFormat(blockFmt);
244 m_cursor.setCharFormat(charFmt);
246 m_cursor.insertBlock(blockFmt, charFmt);
248 qCDebug(lcMD,
"H%d", detail->level);
251 m_needsInsertBlock =
true;
253 MD_BLOCK_LI_DETAIL *detail =
static_cast<MD_BLOCK_LI_DETAIL *>(det);
254 m_markerType = detail->is_task ?
255 (detail->task_mark ==
' ' ? QTextBlockFormat::MarkerType::Unchecked : QTextBlockFormat::MarkerType::Checked) :
256 QTextBlockFormat::MarkerType::NoMarker;
257 qCDebug(lcMD) <<
"LI";
260 if (m_needsInsertList)
261 m_listStack.push(m_cursor.insertList(m_listFormat));
263 m_needsInsertList =
true;
264 MD_BLOCK_UL_DETAIL *detail =
static_cast<MD_BLOCK_UL_DETAIL *>(det);
265 m_listFormat = QTextListFormat();
266 m_listFormat.setIndent(m_listStack.size() + 1);
267 switch (detail->mark) {
269 m_listFormat.setStyle(QTextListFormat::ListCircle);
272 m_listFormat.setStyle(QTextListFormat::ListSquare);
275 m_listFormat.setStyle(QTextListFormat::ListDisc);
278 qCDebug(lcMD,
"UL %c level %d", detail->mark,
int(m_listStack.size()) + 1);
281 if (m_needsInsertList)
282 m_listStack.push(m_cursor.insertList(m_listFormat));
284 m_needsInsertList =
true;
285 MD_BLOCK_OL_DETAIL *detail =
static_cast<MD_BLOCK_OL_DETAIL *>(det);
286 m_listFormat = QTextListFormat();
287 m_listFormat.setIndent(m_listStack.size() + 1);
288 m_listFormat.setNumberSuffix(QChar::fromLatin1(detail->mark_delimiter));
289 m_listFormat.setStyle(QTextListFormat::ListDecimal);
290 m_listFormat.setStart(detail->start);
291 qCDebug(lcMD,
"OL xx%d level %d start %d", detail->mark_delimiter,
int(m_listStack.size()) + 1, detail->start);
294 MD_BLOCK_TD_DETAIL *detail =
static_cast<MD_BLOCK_TD_DETAIL *>(det);
298 QTextTableCell cell = m_currentTable->cellAt(m_tableRowCount - 1, m_tableCol);
299 if (!cell.isValid()) {
300 qWarning(
"malformed table in Markdown input");
303 m_cursor = cell.firstCursorPosition();
304 QTextBlockFormat blockFmt = m_cursor.blockFormat();
305 blockFmt.setAlignment(MdAlignment(detail->align));
306 m_cursor.setBlockFormat(blockFmt);
307 qCDebug(lcMD) <<
"TD; align" << detail->align << MdAlignment(detail->align) <<
"col" << m_tableCol;
310 ++m_tableColumnCount;
312 if (m_currentTable->columns() < m_tableColumnCount)
313 m_currentTable->appendColumns(1);
314 auto cell = m_currentTable->cellAt(m_tableRowCount - 1, m_tableCol);
315 if (!cell.isValid()) {
316 qWarning(
"malformed table in Markdown input");
319 auto fmt = cell.format();
320 fmt.setFontWeight(QFont::Bold);
325 m_nonEmptyTableCells.clear();
326 if (m_currentTable->rows() < m_tableRowCount)
327 m_currentTable->appendRows(1);
329 qCDebug(lcMD) <<
"TR" << m_currentTable->rows();
332 m_tableColumnCount = 0;
334 m_currentTable = m_cursor.insertTable(1, 1);
338 QTextBlockFormat blockFmt;
339 blockFmt.setProperty(QTextFormat::BlockTrailingHorizontalRulerWidth, 1);
340 m_cursor.insertBlock(blockFmt, QTextCharFormat());
348int QTextMarkdownImporter::cbLeaveBlock(
int blockType,
void *detail)
357 if (Q_UNLIKELY(m_needsInsertList))
358 m_listStack.push(m_cursor.createList(m_listFormat));
359 if (Q_UNLIKELY(m_listStack.isEmpty())) {
360 qCWarning(lcMD,
"list ended unexpectedly");
362 qCDebug(lcMD,
"list at level %d ended",
int(m_listStack.size()));
372 for (
int col = m_tableCol; col >= 0; --col) {
373 if (m_nonEmptyTableCells.contains(col)) {
374 if (mergeEnd >= 0 && mergeBegin >= 0) {
375 qCDebug(lcMD) <<
"merging cells" << mergeBegin <<
"to" << mergeEnd <<
"inclusive, on row" << m_currentTable->rows() - 1;
376 m_currentTable->mergeCells(m_currentTable->rows() - 1, mergeBegin - 1, 1, mergeEnd - mergeBegin + 2);
388 case MD_BLOCK_QUOTE: {
389 qCDebug(lcMD,
"QUOTE level %d ended", m_blockQuoteDepth);
391 if (!m_blockQuoteDepth)
392 m_blockQuoteInsideListItem =
false;
393 m_needsInsertBlock =
true;
396 qCDebug(lcMD) <<
"table ended with" << m_currentTable->columns() <<
"cols and" << m_currentTable->rows() <<
"rows";
397 m_currentTable =
nullptr;
398 m_cursor.movePosition(QTextCursor::End);
401 qCDebug(lcMD,
"LI at level %d ended",
int(m_listStack.size()));
404 case MD_BLOCK_CODE: {
406 m_blockCodeLanguage.clear();
407 m_blockCodeFence = 0;
408 if (m_blockQuoteDepth)
409 qCDebug(lcMD,
"CODE ended inside QUOTE %d", m_blockQuoteDepth);
411 qCDebug(lcMD,
"CODE ended");
412 m_needsInsertBlock =
true;
415 m_cursor.setCharFormat(QTextCharFormat());
423int QTextMarkdownImporter::cbEnterSpan(
int spanType,
void *det)
425 QTextCharFormat charFmt;
426 if (!m_spanFormatStack.isEmpty())
427 charFmt = m_spanFormatStack.top();
430 charFmt.setFontItalic(
true);
433 charFmt.setFontWeight(QFont::Bold);
436 charFmt.setFontUnderline(
true);
439 MD_SPAN_A_DETAIL *detail =
static_cast<MD_SPAN_A_DETAIL *>(det);
440 QString url = QString::fromUtf8(detail->href.text,
int(detail->href.size));
441 QString title = QString::fromUtf8(detail->title.text,
int(detail->title.size));
442 charFmt.setAnchor(
true);
443 charFmt.setAnchorHref(url);
444 if (!title.isEmpty())
445 charFmt.setToolTip(title);
446 charFmt.setForeground(m_palette.link());
447 qCDebug(lcMD) <<
"anchor" << url << title;
451 m_imageFormat = QTextImageFormat();
452 MD_SPAN_IMG_DETAIL *detail =
static_cast<MD_SPAN_IMG_DETAIL *>(det);
453 m_imageFormat.setName(QString::fromUtf8(detail->src.text,
int(detail->src.size)));
454 const QVariant tooltip = QString::fromUtf8(detail->title.text,
int(detail->title.size));
455 m_imageFormat.setProperty(QTextFormat::ImageTitle, tooltip);
456 m_imageFormat.setProperty(QTextFormat::TextToolTip, tooltip);
460 charFmt.setFont(m_monoFont);
462 charFmt.clearProperty(QTextFormat::FontPointSize);
463 charFmt.clearProperty(QTextFormat::FontPixelSize);
464 charFmt.setFontFixedPitch(
true);
467 charFmt.setFontStrikeOut(
true);
470 m_spanFormatStack.push(charFmt);
471 qCDebug(lcMD) << spanType <<
"setCharFormat" << charFmt.font().family()
472 << charFmt.fontWeight() << (charFmt.fontItalic() ?
"italic" :
"")
473 << charFmt.foreground().color().name();
474 m_cursor.setCharFormat(charFmt);
478int QTextMarkdownImporter::cbLeaveSpan(
int spanType,
void *detail)
481 QTextCharFormat charFmt;
482 if (!m_spanFormatStack.isEmpty()) {
483 m_spanFormatStack.pop();
484 if (!m_spanFormatStack.isEmpty())
485 charFmt = m_spanFormatStack.top();
487 m_cursor.setCharFormat(charFmt);
488 qCDebug(lcMD) << spanType <<
"setCharFormat" << charFmt.font().family()
489 << charFmt.fontWeight() << (charFmt.fontItalic() ?
"italic" :
"")
490 << charFmt.foreground().color().name();
491 if (spanType ==
int(MD_SPAN_IMG))
496int QTextMarkdownImporter::cbText(
int textType,
const char *text,
unsigned size)
498 if (m_needsInsertBlock)
500#if QT_CONFIG(regularexpression)
501 static const QRegularExpression openingBracket(QStringLiteral(
"<[a-zA-Z]"));
502 static const QRegularExpression closingBracket(QStringLiteral(
"(/>|</)"));
504 QString s = QString::fromUtf8(text,
int(size));
508#if QT_CONFIG(regularexpression)
509 if (m_htmlTagDepth) {
510 m_htmlAccumulator += s;
515 case MD_TEXT_NULLCHAR:
516 s = QString(QChar(u'\xFFFD'));
519 s = QString(qtmi_Newline);
522 s = QString(qtmi_Space);
527#if QT_CONFIG(texthtmlparser)
530 m_htmlAccumulator += s;
532 m_cursor.insertHtml(s);
538#if QT_CONFIG(regularexpression) && QT_CONFIG(texthtmlparser)
540 QRegularExpressionMatchIterator i = openingBracket.globalMatch(s);
541 while (i.hasNext()) {
545 i = closingBracket.globalMatch(s);
546 while (i.hasNext()) {
551 m_htmlAccumulator += s;
552 if (!m_htmlTagDepth) {
553 qCDebug(lcMD) <<
"HTML" << m_htmlAccumulator;
554 m_cursor.insertHtml(m_htmlAccumulator);
555 if (m_spanFormatStack.isEmpty())
556 m_cursor.setCharFormat(QTextCharFormat());
558 m_cursor.setCharFormat(m_spanFormatStack.top());
559 m_htmlAccumulator = QString();
566 switch (m_blockType) {
568 m_nonEmptyTableCells.append(m_tableCol);
571 if (s == qtmi_Newline) {
574 m_needsInsertBlock =
true;
585 m_imageFormat.setProperty(QTextFormat::ImageAltText, s);
586 qCDebug(lcMD) <<
"image" << m_imageFormat.name()
587 <<
"title" << m_imageFormat.stringProperty(QTextFormat::ImageTitle)
588 <<
"alt" << s <<
"relative to" << m_cursor.document()->baseUrl();
589 m_cursor.insertImage(m_imageFormat);
594 m_cursor.insertText(s);
595 if (m_cursor.currentList()) {
597 QTextBlockFormat bfmt = m_cursor.blockFormat();
599 m_cursor.setBlockFormat(bfmt);
601 if (lcMD().isEnabled(QtDebugMsg)) {
602 QTextBlockFormat bfmt = m_cursor.blockFormat();
604 if (m_cursor.currentList())
605 debugInfo =
"in list at depth "_L1 + QString::number(m_cursor.currentList()->format().indent());
606 if (bfmt.hasProperty(QTextFormat::BlockQuoteLevel))
607 debugInfo +=
"in blockquote at depth "_L1 +
608 QString::number(bfmt.intProperty(QTextFormat::BlockQuoteLevel));
609 if (bfmt.hasProperty(QTextFormat::BlockCodeLanguage))
610 debugInfo +=
"in a code block"_L1;
611 qCDebug(lcMD) << textType <<
"in block" << m_blockType << s << qPrintable(debugInfo)
612 <<
"bindent" << bfmt.indent() <<
"tindent" << bfmt.textIndent()
613 <<
"margins" << bfmt.leftMargin() << bfmt.topMargin() << bfmt.bottomMargin() << bfmt.rightMargin();
619
620
621
622
623
624
625
626
627
628
629void QTextMarkdownImporter::insertBlock()
631 QTextCharFormat charFormat;
632 if (!m_spanFormatStack.isEmpty())
633 charFormat = m_spanFormatStack.top();
634 QTextBlockFormat blockFormat;
635 if (!m_listStack.isEmpty() && !m_needsInsertList && m_listItem) {
636 QTextList *list = m_listStack.top();
638 blockFormat = list->item(list->count() - 1).blockFormat();
640 qWarning() <<
"attempted to insert into a list that no longer exists";
642 if (m_blockQuoteDepth) {
643 blockFormat.setProperty(QTextFormat::BlockQuoteLevel, m_blockQuoteDepth);
644 blockFormat.setLeftMargin(qtmi_BlockQuoteIndent * m_blockQuoteDepth);
645 blockFormat.setRightMargin(qtmi_BlockQuoteIndent);
648 if (m_blockQuoteInsideListItem)
649 blockFormat.setProperty(QTextFormat::BlockQuoteInsideListItem,
true);
652 blockFormat.setProperty(QTextFormat::BlockCodeLanguage, m_blockCodeLanguage);
653 if (m_blockCodeFence) {
654 blockFormat.setNonBreakableLines(
true);
655 blockFormat.setProperty(QTextFormat::BlockCodeFence, QString(QLatin1Char(m_blockCodeFence)));
657 charFormat.setFont(m_monoFont);
659 charFormat.clearProperty(QTextFormat::FontPointSize);
660 charFormat.clearProperty(QTextFormat::FontPixelSize);
662 blockFormat.clearProperty(QTextFormat::BlockCodeLanguage);
663 blockFormat.clearProperty(QTextFormat::BlockCodeFence);
664 blockFormat.setNonBreakableLines(
false);
665 blockFormat.setTopMargin(m_paragraphMargin);
666 blockFormat.setBottomMargin(m_paragraphMargin);
668 if (m_markerType == QTextBlockFormat::MarkerType::NoMarker)
669 blockFormat.clearProperty(QTextFormat::BlockMarker);
671 blockFormat.setMarker(m_markerType);
672 if (!m_listStack.isEmpty())
673 blockFormat.setIndent(m_listStack.size());
674 if (m_cursor.document()->isEmpty()) {
675 m_cursor.setBlockFormat(blockFormat);
676 m_cursor.setCharFormat(charFormat);
677 }
else if (m_listItem) {
678 m_cursor.insertBlock(blockFormat, QTextCharFormat());
679 m_cursor.setCharFormat(charFormat);
681 m_cursor.insertBlock(blockFormat, charFormat);
683 if (m_needsInsertList) {
684 m_listStack.push(m_cursor.createList(m_listFormat));
685 }
else if (!m_listStack.isEmpty() && m_listItem && m_listStack.top()) {
686 m_listStack.top()->add(m_cursor.block());
688 m_needsInsertList =
false;
689 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)