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 if (defaultFont.pointSize() != -1)
184 m_monoFont.setPointSize(defaultFont.pointSize());
186 m_monoFont.setPixelSize(defaultFont.pixelSize());
187 qCDebug(lcMD) <<
"default font" << defaultFont <<
"mono font" << m_monoFont;
188 QStringView md = markdown;
190 if (m_features.testFlag(QTextMarkdownImporter::FeatureFrontMatter)) {
191 if (
const auto split = splitFrontMatter(md)) {
192 doc->setMetaInformation(QTextDocument::FrontMatter, split.frontMatter.toString());
193 qCDebug(lcMD) <<
"extracted FrontMatter: size" << split.frontMatter.size();
198 const auto mdUtf8 = md.toUtf8();
199 m_cursor.beginEditBlock();
200 md_parse(mdUtf8.constData(), MD_SIZE(mdUtf8.size()), &callbacks,
this);
201 m_cursor.endEditBlock();
204int QTextMarkdownImporter::cbEnterBlock(
int blockType,
void *det)
206 m_blockType = blockType;
209 if (!m_listStack.isEmpty())
210 qCDebug(lcMD, m_listItem ?
"P of LI at level %d" :
"P continuation inside LI at level %d",
int(m_listStack.size()));
213 m_needsInsertBlock =
true;
217 qCDebug(lcMD,
"QUOTE level %d", m_blockQuoteDepth);
219 case MD_BLOCK_CODE: {
220 MD_BLOCK_CODE_DETAIL *detail =
static_cast<MD_BLOCK_CODE_DETAIL *>(det);
222 m_blockCodeLanguage = QLatin1StringView(detail->lang.text,
int(detail->lang.size));
223 m_blockCodeFence = detail->fence_char;
224 QString info = QLatin1StringView(detail->info.text,
int(detail->info.size));
225 m_needsInsertBlock =
true;
226 if (m_blockQuoteDepth)
227 qCDebug(lcMD,
"CODE lang '%s' info '%s' fenced with '%c' inside QUOTE %d", qPrintable(m_blockCodeLanguage), qPrintable(info), m_blockCodeFence, m_blockQuoteDepth);
229 qCDebug(lcMD,
"CODE lang '%s' info '%s' fenced with '%c'", qPrintable(m_blockCodeLanguage), qPrintable(info), m_blockCodeFence);
232 MD_BLOCK_H_DETAIL *detail =
static_cast<MD_BLOCK_H_DETAIL *>(det);
233 QTextBlockFormat blockFmt;
234 QTextCharFormat charFmt;
235 int sizeAdjustment = 4 -
int(detail->level);
236 charFmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment);
237 charFmt.setFontWeight(QFont::Bold);
238 blockFmt.setHeadingLevel(
int(detail->level));
239 m_needsInsertBlock =
false;
240 if (m_cursor.document()->isEmpty()) {
241 m_cursor.setBlockFormat(blockFmt);
242 m_cursor.setCharFormat(charFmt);
244 m_cursor.insertBlock(blockFmt, charFmt);
246 qCDebug(lcMD,
"H%d", detail->level);
249 m_needsInsertBlock =
true;
251 MD_BLOCK_LI_DETAIL *detail =
static_cast<MD_BLOCK_LI_DETAIL *>(det);
252 m_markerType = detail->is_task ?
253 (detail->task_mark ==
' ' ? QTextBlockFormat::MarkerType::Unchecked : QTextBlockFormat::MarkerType::Checked) :
254 QTextBlockFormat::MarkerType::NoMarker;
255 qCDebug(lcMD) <<
"LI";
258 if (m_needsInsertList)
259 m_listStack.push(m_cursor.insertList(m_listFormat));
261 m_needsInsertList =
true;
262 MD_BLOCK_UL_DETAIL *detail =
static_cast<MD_BLOCK_UL_DETAIL *>(det);
263 m_listFormat = QTextListFormat();
264 m_listFormat.setIndent(m_listStack.size() + 1);
265 switch (detail->mark) {
267 m_listFormat.setStyle(QTextListFormat::ListCircle);
270 m_listFormat.setStyle(QTextListFormat::ListSquare);
273 m_listFormat.setStyle(QTextListFormat::ListDisc);
276 qCDebug(lcMD,
"UL %c level %d", detail->mark,
int(m_listStack.size()) + 1);
279 if (m_needsInsertList)
280 m_listStack.push(m_cursor.insertList(m_listFormat));
282 m_needsInsertList =
true;
283 MD_BLOCK_OL_DETAIL *detail =
static_cast<MD_BLOCK_OL_DETAIL *>(det);
284 m_listFormat = QTextListFormat();
285 m_listFormat.setIndent(m_listStack.size() + 1);
286 m_listFormat.setNumberSuffix(QChar::fromLatin1(detail->mark_delimiter));
287 m_listFormat.setStyle(QTextListFormat::ListDecimal);
288 m_listFormat.setStart(detail->start);
289 qCDebug(lcMD,
"OL xx%d level %d start %d", detail->mark_delimiter,
int(m_listStack.size()) + 1, detail->start);
292 MD_BLOCK_TD_DETAIL *detail =
static_cast<MD_BLOCK_TD_DETAIL *>(det);
296 QTextTableCell cell = m_currentTable->cellAt(m_tableRowCount - 1, m_tableCol);
297 if (!cell.isValid()) {
298 qWarning(
"malformed table in Markdown input");
301 m_cursor = cell.firstCursorPosition();
302 QTextBlockFormat blockFmt = m_cursor.blockFormat();
303 blockFmt.setAlignment(MdAlignment(detail->align));
304 m_cursor.setBlockFormat(blockFmt);
305 qCDebug(lcMD) <<
"TD; align" << detail->align << MdAlignment(detail->align) <<
"col" << m_tableCol;
308 ++m_tableColumnCount;
310 if (m_currentTable->columns() < m_tableColumnCount)
311 m_currentTable->appendColumns(1);
312 auto cell = m_currentTable->cellAt(m_tableRowCount - 1, m_tableCol);
313 if (!cell.isValid()) {
314 qWarning(
"malformed table in Markdown input");
317 auto fmt = cell.format();
318 fmt.setFontWeight(QFont::Bold);
323 m_nonEmptyTableCells.clear();
324 if (m_currentTable->rows() < m_tableRowCount)
325 m_currentTable->appendRows(1);
327 qCDebug(lcMD) <<
"TR" << m_currentTable->rows();
330 m_tableColumnCount = 0;
332 m_currentTable = m_cursor.insertTable(1, 1);
336 QTextBlockFormat blockFmt;
337 blockFmt.setProperty(QTextFormat::BlockTrailingHorizontalRulerWidth, 1);
338 m_cursor.insertBlock(blockFmt, QTextCharFormat());
346int QTextMarkdownImporter::cbLeaveBlock(
int blockType,
void *detail)
355 if (Q_UNLIKELY(m_needsInsertList))
356 m_listStack.push(m_cursor.createList(m_listFormat));
357 if (Q_UNLIKELY(m_listStack.isEmpty())) {
358 qCWarning(lcMD,
"list ended unexpectedly");
360 qCDebug(lcMD,
"list at level %d ended",
int(m_listStack.size()));
370 for (
int col = m_tableCol; col >= 0; --col) {
371 if (m_nonEmptyTableCells.contains(col)) {
372 if (mergeEnd >= 0 && mergeBegin >= 0) {
373 qCDebug(lcMD) <<
"merging cells" << mergeBegin <<
"to" << mergeEnd <<
"inclusive, on row" << m_currentTable->rows() - 1;
374 m_currentTable->mergeCells(m_currentTable->rows() - 1, mergeBegin - 1, 1, mergeEnd - mergeBegin + 2);
386 case MD_BLOCK_QUOTE: {
387 qCDebug(lcMD,
"QUOTE level %d ended", m_blockQuoteDepth);
389 m_needsInsertBlock =
true;
392 qCDebug(lcMD) <<
"table ended with" << m_currentTable->columns() <<
"cols and" << m_currentTable->rows() <<
"rows";
393 m_currentTable =
nullptr;
394 m_cursor.movePosition(QTextCursor::End);
397 qCDebug(lcMD,
"LI at level %d ended",
int(m_listStack.size()));
400 case MD_BLOCK_CODE: {
402 m_blockCodeLanguage.clear();
403 m_blockCodeFence = 0;
404 if (m_blockQuoteDepth)
405 qCDebug(lcMD,
"CODE ended inside QUOTE %d", m_blockQuoteDepth);
407 qCDebug(lcMD,
"CODE ended");
408 m_needsInsertBlock =
true;
411 m_cursor.setCharFormat(QTextCharFormat());
419int QTextMarkdownImporter::cbEnterSpan(
int spanType,
void *det)
421 QTextCharFormat charFmt;
422 if (!m_spanFormatStack.isEmpty())
423 charFmt = m_spanFormatStack.top();
426 charFmt.setFontItalic(
true);
429 charFmt.setFontWeight(QFont::Bold);
432 charFmt.setFontUnderline(
true);
435 MD_SPAN_A_DETAIL *detail =
static_cast<MD_SPAN_A_DETAIL *>(det);
436 QString url = QString::fromUtf8(detail->href.text,
int(detail->href.size));
437 QString title = QString::fromUtf8(detail->title.text,
int(detail->title.size));
438 charFmt.setAnchor(
true);
439 charFmt.setAnchorHref(url);
440 if (!title.isEmpty())
441 charFmt.setToolTip(title);
442 charFmt.setForeground(m_palette.link());
443 qCDebug(lcMD) <<
"anchor" << url << title;
447 m_imageFormat = QTextImageFormat();
448 MD_SPAN_IMG_DETAIL *detail =
static_cast<MD_SPAN_IMG_DETAIL *>(det);
449 m_imageFormat.setName(QString::fromUtf8(detail->src.text,
int(detail->src.size)));
450 m_imageFormat.setProperty(QTextFormat::ImageTitle, QString::fromUtf8(detail->title.text,
int(detail->title.size)));
454 charFmt.setFont(m_monoFont);
455 charFmt.setFontFixedPitch(
true);
458 charFmt.setFontStrikeOut(
true);
461 m_spanFormatStack.push(charFmt);
462 qCDebug(lcMD) << spanType <<
"setCharFormat" << charFmt.font().family()
463 << charFmt.fontWeight() << (charFmt.fontItalic() ?
"italic" :
"")
464 << charFmt.foreground().color().name();
465 m_cursor.setCharFormat(charFmt);
469int QTextMarkdownImporter::cbLeaveSpan(
int spanType,
void *detail)
472 QTextCharFormat charFmt;
473 if (!m_spanFormatStack.isEmpty()) {
474 m_spanFormatStack.pop();
475 if (!m_spanFormatStack.isEmpty())
476 charFmt = m_spanFormatStack.top();
478 m_cursor.setCharFormat(charFmt);
479 qCDebug(lcMD) << spanType <<
"setCharFormat" << charFmt.font().family()
480 << charFmt.fontWeight() << (charFmt.fontItalic() ?
"italic" :
"")
481 << charFmt.foreground().color().name();
482 if (spanType ==
int(MD_SPAN_IMG))
487int QTextMarkdownImporter::cbText(
int textType,
const char *text,
unsigned size)
489 if (m_needsInsertBlock)
491#if QT_CONFIG(regularexpression)
492 static const QRegularExpression openingBracket(QStringLiteral(
"<[a-zA-Z]"));
493 static const QRegularExpression closingBracket(QStringLiteral(
"(/>|</)"));
495 QString s = QString::fromUtf8(text,
int(size));
499#if QT_CONFIG(regularexpression)
500 if (m_htmlTagDepth) {
501 m_htmlAccumulator += s;
506 case MD_TEXT_NULLCHAR:
507 s = QString(QChar(u'\xFFFD'));
510 s = QString(qtmi_Newline);
513 s = QString(qtmi_Space);
518#if QT_CONFIG(texthtmlparser)
521 m_htmlAccumulator += s;
523 m_cursor.insertHtml(s);
529#if QT_CONFIG(regularexpression) && QT_CONFIG(texthtmlparser)
531 QRegularExpressionMatchIterator i = openingBracket.globalMatch(s);
532 while (i.hasNext()) {
536 i = closingBracket.globalMatch(s);
537 while (i.hasNext()) {
542 m_htmlAccumulator += s;
543 if (!m_htmlTagDepth) {
544 qCDebug(lcMD) <<
"HTML" << m_htmlAccumulator;
545 m_cursor.insertHtml(m_htmlAccumulator);
546 if (m_spanFormatStack.isEmpty())
547 m_cursor.setCharFormat(QTextCharFormat());
549 m_cursor.setCharFormat(m_spanFormatStack.top());
550 m_htmlAccumulator = QString();
557 switch (m_blockType) {
559 m_nonEmptyTableCells.append(m_tableCol);
562 if (s == qtmi_Newline) {
565 m_needsInsertBlock =
true;
576 m_imageFormat.setProperty(QTextFormat::ImageAltText, s);
577 qCDebug(lcMD) <<
"image" << m_imageFormat.name()
578 <<
"title" << m_imageFormat.stringProperty(QTextFormat::ImageTitle)
579 <<
"alt" << s <<
"relative to" << m_cursor.document()->baseUrl();
580 m_cursor.insertImage(m_imageFormat);
585 m_cursor.insertText(s);
586 if (m_cursor.currentList()) {
588 QTextBlockFormat bfmt = m_cursor.blockFormat();
590 m_cursor.setBlockFormat(bfmt);
592 if (lcMD().isEnabled(QtDebugMsg)) {
593 QTextBlockFormat bfmt = m_cursor.blockFormat();
595 if (m_cursor.currentList())
596 debugInfo =
"in list at depth "_L1 + QString::number(m_cursor.currentList()->format().indent());
597 if (bfmt.hasProperty(QTextFormat::BlockQuoteLevel))
598 debugInfo +=
"in blockquote at depth "_L1 +
599 QString::number(bfmt.intProperty(QTextFormat::BlockQuoteLevel));
600 if (bfmt.hasProperty(QTextFormat::BlockCodeLanguage))
601 debugInfo +=
"in a code block"_L1;
602 qCDebug(lcMD) << textType <<
"in block" << m_blockType << s << qPrintable(debugInfo)
603 <<
"bindent" << bfmt.indent() <<
"tindent" << bfmt.textIndent()
604 <<
"margins" << bfmt.leftMargin() << bfmt.topMargin() << bfmt.bottomMargin() << bfmt.rightMargin();
610
611
612
613
614
615
616
617
618
619
620void QTextMarkdownImporter::insertBlock()
622 QTextCharFormat charFormat;
623 if (!m_spanFormatStack.isEmpty())
624 charFormat = m_spanFormatStack.top();
625 QTextBlockFormat blockFormat;
626 if (!m_listStack.isEmpty() && !m_needsInsertList && m_listItem) {
627 QTextList *list = m_listStack.top();
629 blockFormat = list->item(list->count() - 1).blockFormat();
631 qWarning() <<
"attempted to insert into a list that no longer exists";
633 if (m_blockQuoteDepth) {
634 blockFormat.setProperty(QTextFormat::BlockQuoteLevel, m_blockQuoteDepth);
635 blockFormat.setLeftMargin(qtmi_BlockQuoteIndent * m_blockQuoteDepth);
636 blockFormat.setRightMargin(qtmi_BlockQuoteIndent);
639 blockFormat.setProperty(QTextFormat::BlockCodeLanguage, m_blockCodeLanguage);
640 if (m_blockCodeFence) {
641 blockFormat.setNonBreakableLines(
true);
642 blockFormat.setProperty(QTextFormat::BlockCodeFence, QString(QLatin1Char(m_blockCodeFence)));
644 charFormat.setFont(m_monoFont);
646 blockFormat.setTopMargin(m_paragraphMargin);
647 blockFormat.setBottomMargin(m_paragraphMargin);
649 if (m_markerType == QTextBlockFormat::MarkerType::NoMarker)
650 blockFormat.clearProperty(QTextFormat::BlockMarker);
652 blockFormat.setMarker(m_markerType);
653 if (!m_listStack.isEmpty())
654 blockFormat.setIndent(m_listStack.size());
655 if (m_cursor.document()->isEmpty()) {
656 m_cursor.setBlockFormat(blockFormat);
657 m_cursor.setCharFormat(charFormat);
658 }
else if (m_listItem) {
659 m_cursor.insertBlock(blockFormat, QTextCharFormat());
660 m_cursor.setCharFormat(charFormat);
662 m_cursor.insertBlock(blockFormat, charFormat);
664 if (m_needsInsertList) {
665 m_listStack.push(m_cursor.createList(m_listFormat));
666 }
else if (!m_listStack.isEmpty() && m_listItem && m_listStack.top()) {
667 m_listStack.top()->add(m_cursor.block());
669 m_needsInsertList =
false;
670 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)