1// Copyright (C) 2019 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qtextmarkdownimporter_p.h"
5#include "qtextdocumentfragment_p.h"
6#include <QLoggingCategory>
7#if QT_CONFIG(regularexpression)
8#include <QRegularExpression>
9#endif
10#include <QTextCursor>
11#include <QTextDocument>
12#include <QTextDocumentFragment>
13#include <QTextList>
14#include <QTextTable>
15#if QT_CONFIG(system_textmarkdownreader)
16#include <md4c.h>
17#else
18#include "../../3rdparty/md4c/md4c.h"
19#endif
20
21QT_BEGIN_NAMESPACE
22
23using namespace Qt::StringLiterals;
24
25Q_LOGGING_CATEGORY(lcMD, "qt.text.markdown")
26
27static const QChar qtmi_Newline = u'\n';
28static const QChar qtmi_Space = u' ';
29
30static constexpr auto markerString() noexcept { return "---"_L1; }
31
32// TODO maybe eliminate the margins after all views recognize BlockQuoteLevel, CSS can format it, etc.
33static const int qtmi_BlockQuoteIndent =
34 40; // pixels, same as in QTextHtmlParserNode::initializeProperties
35
36static_assert(int(QTextMarkdownImporter::FeatureCollapseWhitespace) == MD_FLAG_COLLAPSEWHITESPACE);
37static_assert(int(QTextMarkdownImporter::FeaturePermissiveATXHeaders) == MD_FLAG_PERMISSIVEATXHEADERS);
38static_assert(int(QTextMarkdownImporter::FeaturePermissiveURLAutoLinks) == MD_FLAG_PERMISSIVEURLAUTOLINKS);
39static_assert(int(QTextMarkdownImporter::FeaturePermissiveMailAutoLinks) == MD_FLAG_PERMISSIVEEMAILAUTOLINKS);
40static_assert(int(QTextMarkdownImporter::FeatureNoIndentedCodeBlocks) == MD_FLAG_NOINDENTEDCODEBLOCKS);
41static_assert(int(QTextMarkdownImporter::FeatureNoHTMLBlocks) == MD_FLAG_NOHTMLBLOCKS);
42static_assert(int(QTextMarkdownImporter::FeatureNoHTMLSpans) == MD_FLAG_NOHTMLSPANS);
43static_assert(int(QTextMarkdownImporter::FeatureTables) == MD_FLAG_TABLES);
44static_assert(int(QTextMarkdownImporter::FeatureStrikeThrough) == MD_FLAG_STRIKETHROUGH);
45static_assert(int(QTextMarkdownImporter::FeatureUnderline) == MD_FLAG_UNDERLINE);
46static_assert(int(QTextMarkdownImporter::FeaturePermissiveWWWAutoLinks) == MD_FLAG_PERMISSIVEWWWAUTOLINKS);
47static_assert(int(QTextMarkdownImporter::FeaturePermissiveAutoLinks) == MD_FLAG_PERMISSIVEAUTOLINKS);
48static_assert(int(QTextMarkdownImporter::FeatureTasklists) == MD_FLAG_TASKLISTS);
49static_assert(int(QTextMarkdownImporter::FeatureNoHTML) == MD_FLAG_NOHTML);
50static_assert(int(QTextMarkdownImporter::DialectCommonMark) == MD_DIALECT_COMMONMARK);
51static_assert(int(QTextMarkdownImporter::DialectGitHub) ==
52 (MD_DIALECT_GITHUB | MD_FLAG_UNDERLINE | QTextMarkdownImporter::FeatureFrontMatter));
53
54// --------------------------------------------------------
55// MD4C callback function wrappers
56
57static int CbEnterBlock(MD_BLOCKTYPE type, void *detail, void *userdata)
58{
59 QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata);
60 return mdi->cbEnterBlock(blockType: int(type), detail);
61}
62
63static int CbLeaveBlock(MD_BLOCKTYPE type, void *detail, void *userdata)
64{
65 QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata);
66 return mdi->cbLeaveBlock(blockType: int(type), detail);
67}
68
69static int CbEnterSpan(MD_SPANTYPE type, void *detail, void *userdata)
70{
71 QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata);
72 return mdi->cbEnterSpan(spanType: int(type), detail);
73}
74
75static int CbLeaveSpan(MD_SPANTYPE type, void *detail, void *userdata)
76{
77 QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata);
78 return mdi->cbLeaveSpan(spanType: int(type), detail);
79}
80
81static int CbText(MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size, void *userdata)
82{
83 QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata);
84 return mdi->cbText(textType: int(type), text, size);
85}
86
87static void CbDebugLog(const char *msg, void *userdata)
88{
89 Q_UNUSED(userdata);
90 qCDebug(lcMD) << msg;
91}
92
93// MD4C callback function wrappers
94// --------------------------------------------------------
95
96static Qt::Alignment MdAlignment(MD_ALIGN a, Qt::Alignment defaultAlignment = Qt::AlignLeft | Qt::AlignVCenter)
97{
98 switch (a) {
99 case MD_ALIGN_LEFT:
100 return Qt::AlignLeft | Qt::AlignVCenter;
101 case MD_ALIGN_CENTER:
102 return Qt::AlignHCenter | Qt::AlignVCenter;
103 case MD_ALIGN_RIGHT:
104 return Qt::AlignRight | Qt::AlignVCenter;
105 default: // including MD_ALIGN_DEFAULT
106 return defaultAlignment;
107 }
108}
109
110QTextMarkdownImporter::QTextMarkdownImporter(QTextDocument *doc, QTextMarkdownImporter::Features features)
111 : m_cursor(doc)
112 , m_monoFont(QFontDatabase::systemFont(type: QFontDatabase::FixedFont))
113 , m_features(features)
114{
115}
116
117QTextMarkdownImporter::QTextMarkdownImporter(QTextDocument *doc, QTextDocument::MarkdownFeatures features)
118 : QTextMarkdownImporter(doc, static_cast<QTextMarkdownImporter::Features>(int(features)))
119{
120}
121
122void QTextMarkdownImporter::import(const QString &markdown)
123{
124 MD_PARSER callbacks = {
125 .abi_version: 0, // abi_version
126 .flags: unsigned(m_features),
127 .enter_block: &CbEnterBlock,
128 .leave_block: &CbLeaveBlock,
129 .enter_span: &CbEnterSpan,
130 .leave_span: &CbLeaveSpan,
131 .text: &CbText,
132 .debug_log: &CbDebugLog,
133 .syntax: nullptr // syntax
134 };
135 QTextDocument *doc = m_cursor.document();
136 const auto defaultFont = doc->defaultFont();
137 m_paragraphMargin = defaultFont.pointSize() * 2 / 3;
138 doc->clear();
139 if (defaultFont.pointSize() != -1)
140 m_monoFont.setPointSize(defaultFont.pointSize());
141 else
142 m_monoFont.setPixelSize(defaultFont.pixelSize());
143 qCDebug(lcMD) << "default font" << defaultFont << "mono font" << m_monoFont;
144 QStringView md = markdown;
145
146 if (m_features.testFlag(flag: QTextMarkdownImporter::FeatureFrontMatter) && md.startsWith(s: markerString())) {
147 qsizetype endMarkerPos = md.indexOf(s: markerString(), from: markerString().size() + 1);
148 if (endMarkerPos > 4) {
149 qsizetype firstLinePos = 4; // first line of yaml
150 while (md.at(n: firstLinePos) == '\n'_L1 || md.at(n: firstLinePos) == '\r'_L1)
151 ++firstLinePos;
152 auto frontMatter = md.sliced(pos: firstLinePos, n: endMarkerPos - firstLinePos);
153 firstLinePos = endMarkerPos + 4; // first line of markdown after yaml
154 while (md.size() > firstLinePos && (md.at(n: firstLinePos) == '\n'_L1 || md.at(n: firstLinePos) == '\r'_L1))
155 ++firstLinePos;
156 md = md.sliced(pos: firstLinePos);
157 doc->setMetaInformation(info: QTextDocument::FrontMatter, frontMatter.toString());
158 qCDebug(lcMD) << "extracted FrontMatter: size" << frontMatter.size();
159 }
160 }
161 const auto mdUtf8 = md.toUtf8();
162 m_cursor.beginEditBlock();
163 md_parse(text: mdUtf8.constData(), size: MD_SIZE(mdUtf8.size()), parser: &callbacks, userdata: this);
164 m_cursor.endEditBlock();
165}
166
167int QTextMarkdownImporter::cbEnterBlock(int blockType, void *det)
168{
169 m_blockType = blockType;
170 switch (blockType) {
171 case MD_BLOCK_P:
172 if (!m_listStack.isEmpty())
173 qCDebug(lcMD, m_listItem ? "P of LI at level %d" : "P continuation inside LI at level %d", int(m_listStack.size()));
174 else
175 qCDebug(lcMD, "P");
176 m_needsInsertBlock = true;
177 break;
178 case MD_BLOCK_QUOTE:
179 ++m_blockQuoteDepth;
180 qCDebug(lcMD, "QUOTE level %d", m_blockQuoteDepth);
181 break;
182 case MD_BLOCK_CODE: {
183 MD_BLOCK_CODE_DETAIL *detail = static_cast<MD_BLOCK_CODE_DETAIL *>(det);
184 m_codeBlock = true;
185 m_blockCodeLanguage = QLatin1StringView(detail->lang.text, int(detail->lang.size));
186 m_blockCodeFence = detail->fence_char;
187 QString info = QLatin1StringView(detail->info.text, int(detail->info.size));
188 m_needsInsertBlock = true;
189 if (m_blockQuoteDepth)
190 qCDebug(lcMD, "CODE lang '%s' info '%s' fenced with '%c' inside QUOTE %d", qPrintable(m_blockCodeLanguage), qPrintable(info), m_blockCodeFence, m_blockQuoteDepth);
191 else
192 qCDebug(lcMD, "CODE lang '%s' info '%s' fenced with '%c'", qPrintable(m_blockCodeLanguage), qPrintable(info), m_blockCodeFence);
193 } break;
194 case MD_BLOCK_H: {
195 MD_BLOCK_H_DETAIL *detail = static_cast<MD_BLOCK_H_DETAIL *>(det);
196 QTextBlockFormat blockFmt;
197 QTextCharFormat charFmt;
198 int sizeAdjustment = 4 - int(detail->level); // H1 to H6: +3 to -2
199 charFmt.setProperty(propertyId: QTextFormat::FontSizeAdjustment, value: sizeAdjustment);
200 charFmt.setFontWeight(QFont::Bold);
201 blockFmt.setHeadingLevel(int(detail->level));
202 m_needsInsertBlock = false;
203 if (m_cursor.document()->isEmpty()) {
204 m_cursor.setBlockFormat(blockFmt);
205 m_cursor.setCharFormat(charFmt);
206 } else {
207 m_cursor.insertBlock(format: blockFmt, charFormat: charFmt);
208 }
209 qCDebug(lcMD, "H%d", detail->level);
210 } break;
211 case MD_BLOCK_LI: {
212 m_needsInsertBlock = true;
213 m_listItem = true;
214 MD_BLOCK_LI_DETAIL *detail = static_cast<MD_BLOCK_LI_DETAIL *>(det);
215 m_markerType = detail->is_task ?
216 (detail->task_mark == ' ' ? QTextBlockFormat::MarkerType::Unchecked : QTextBlockFormat::MarkerType::Checked) :
217 QTextBlockFormat::MarkerType::NoMarker;
218 qCDebug(lcMD) << "LI";
219 } break;
220 case MD_BLOCK_UL: {
221 if (m_needsInsertList) // list nested in an empty list
222 m_listStack.push(t: m_cursor.insertList(format: m_listFormat));
223 else
224 m_needsInsertList = true;
225 MD_BLOCK_UL_DETAIL *detail = static_cast<MD_BLOCK_UL_DETAIL *>(det);
226 m_listFormat = QTextListFormat();
227 m_listFormat.setIndent(m_listStack.size() + 1);
228 switch (detail->mark) {
229 case '*':
230 m_listFormat.setStyle(QTextListFormat::ListCircle);
231 break;
232 case '+':
233 m_listFormat.setStyle(QTextListFormat::ListSquare);
234 break;
235 default: // including '-'
236 m_listFormat.setStyle(QTextListFormat::ListDisc);
237 break;
238 }
239 qCDebug(lcMD, "UL %c level %d", detail->mark, int(m_listStack.size()) + 1);
240 } break;
241 case MD_BLOCK_OL: {
242 if (m_needsInsertList) // list nested in an empty list
243 m_listStack.push(t: m_cursor.insertList(format: m_listFormat));
244 else
245 m_needsInsertList = true;
246 MD_BLOCK_OL_DETAIL *detail = static_cast<MD_BLOCK_OL_DETAIL *>(det);
247 m_listFormat = QTextListFormat();
248 m_listFormat.setIndent(m_listStack.size() + 1);
249 m_listFormat.setNumberSuffix(QChar::fromLatin1(c: detail->mark_delimiter));
250 m_listFormat.setStyle(QTextListFormat::ListDecimal);
251 m_listFormat.setStart(detail->start);
252 qCDebug(lcMD, "OL xx%d level %d start %d", detail->mark_delimiter, int(m_listStack.size()) + 1, detail->start);
253 } break;
254 case MD_BLOCK_TD: {
255 MD_BLOCK_TD_DETAIL *detail = static_cast<MD_BLOCK_TD_DETAIL *>(det);
256 ++m_tableCol;
257 // absolute movement (and storage of m_tableCol) shouldn't be necessary, but
258 // movePosition(QTextCursor::NextCell) doesn't work
259 QTextTableCell cell = m_currentTable->cellAt(row: m_tableRowCount - 1, col: m_tableCol);
260 if (!cell.isValid()) {
261 qWarning(msg: "malformed table in Markdown input");
262 return 1;
263 }
264 m_cursor = cell.firstCursorPosition();
265 QTextBlockFormat blockFmt = m_cursor.blockFormat();
266 blockFmt.setAlignment(MdAlignment(a: detail->align));
267 m_cursor.setBlockFormat(blockFmt);
268 qCDebug(lcMD) << "TD; align" << detail->align << MdAlignment(a: detail->align) << "col" << m_tableCol;
269 } break;
270 case MD_BLOCK_TH: {
271 ++m_tableColumnCount;
272 ++m_tableCol;
273 if (m_currentTable->columns() < m_tableColumnCount)
274 m_currentTable->appendColumns(count: 1);
275 auto cell = m_currentTable->cellAt(row: m_tableRowCount - 1, col: m_tableCol);
276 if (!cell.isValid()) {
277 qWarning(msg: "malformed table in Markdown input");
278 return 1;
279 }
280 auto fmt = cell.format();
281 fmt.setFontWeight(QFont::Bold);
282 cell.setFormat(fmt);
283 } break;
284 case MD_BLOCK_TR: {
285 ++m_tableRowCount;
286 m_nonEmptyTableCells.clear();
287 if (m_currentTable->rows() < m_tableRowCount)
288 m_currentTable->appendRows(count: 1);
289 m_tableCol = -1;
290 qCDebug(lcMD) << "TR" << m_currentTable->rows();
291 } break;
292 case MD_BLOCK_TABLE:
293 m_tableColumnCount = 0;
294 m_tableRowCount = 0;
295 m_currentTable = m_cursor.insertTable(rows: 1, cols: 1); // we don't know the dimensions yet
296 break;
297 case MD_BLOCK_HR: {
298 qCDebug(lcMD, "HR");
299 QTextBlockFormat blockFmt;
300 blockFmt.setProperty(propertyId: QTextFormat::BlockTrailingHorizontalRulerWidth, value: 1);
301 m_cursor.insertBlock(format: blockFmt, charFormat: QTextCharFormat());
302 } break;
303 default:
304 break; // nothing to do for now
305 }
306 return 0; // no error
307}
308
309int QTextMarkdownImporter::cbLeaveBlock(int blockType, void *detail)
310{
311 Q_UNUSED(detail);
312 switch (blockType) {
313 case MD_BLOCK_P:
314 m_listItem = false;
315 break;
316 case MD_BLOCK_UL:
317 case MD_BLOCK_OL:
318 if (Q_UNLIKELY(m_needsInsertList))
319 m_listStack.push(t: m_cursor.createList(format: m_listFormat));
320 if (Q_UNLIKELY(m_listStack.isEmpty())) {
321 qCWarning(lcMD, "list ended unexpectedly");
322 } else {
323 qCDebug(lcMD, "list at level %d ended", int(m_listStack.size()));
324 m_listStack.pop();
325 }
326 break;
327 case MD_BLOCK_TR: {
328 // https://github.com/mity/md4c/issues/29
329 // MD4C doesn't tell us explicitly which cells are merged, so merge empty cells
330 // with previous non-empty ones
331 int mergeEnd = -1;
332 int mergeBegin = -1;
333 for (int col = m_tableCol; col >= 0; --col) {
334 if (m_nonEmptyTableCells.contains(t: col)) {
335 if (mergeEnd >= 0 && mergeBegin >= 0) {
336 qCDebug(lcMD) << "merging cells" << mergeBegin << "to" << mergeEnd << "inclusive, on row" << m_currentTable->rows() - 1;
337 m_currentTable->mergeCells(row: m_currentTable->rows() - 1, col: mergeBegin - 1, numRows: 1, numCols: mergeEnd - mergeBegin + 2);
338 }
339 mergeEnd = -1;
340 mergeBegin = -1;
341 } else {
342 if (mergeEnd < 0)
343 mergeEnd = col;
344 else
345 mergeBegin = col;
346 }
347 }
348 } break;
349 case MD_BLOCK_QUOTE: {
350 qCDebug(lcMD, "QUOTE level %d ended", m_blockQuoteDepth);
351 --m_blockQuoteDepth;
352 m_needsInsertBlock = true;
353 } break;
354 case MD_BLOCK_TABLE:
355 qCDebug(lcMD) << "table ended with" << m_currentTable->columns() << "cols and" << m_currentTable->rows() << "rows";
356 m_currentTable = nullptr;
357 m_cursor.movePosition(op: QTextCursor::End);
358 break;
359 case MD_BLOCK_LI:
360 qCDebug(lcMD, "LI at level %d ended", int(m_listStack.size()));
361 m_listItem = false;
362 break;
363 case MD_BLOCK_CODE: {
364 m_codeBlock = false;
365 m_blockCodeLanguage.clear();
366 m_blockCodeFence = 0;
367 if (m_blockQuoteDepth)
368 qCDebug(lcMD, "CODE ended inside QUOTE %d", m_blockQuoteDepth);
369 else
370 qCDebug(lcMD, "CODE ended");
371 m_needsInsertBlock = true;
372 } break;
373 case MD_BLOCK_H:
374 m_cursor.setCharFormat(QTextCharFormat());
375 break;
376 default:
377 break;
378 }
379 return 0; // no error
380}
381
382int QTextMarkdownImporter::cbEnterSpan(int spanType, void *det)
383{
384 QTextCharFormat charFmt;
385 if (!m_spanFormatStack.isEmpty())
386 charFmt = m_spanFormatStack.top();
387 switch (spanType) {
388 case MD_SPAN_EM:
389 charFmt.setFontItalic(true);
390 break;
391 case MD_SPAN_STRONG:
392 charFmt.setFontWeight(QFont::Bold);
393 break;
394 case MD_SPAN_U:
395 charFmt.setFontUnderline(true);
396 break;
397 case MD_SPAN_A: {
398 MD_SPAN_A_DETAIL *detail = static_cast<MD_SPAN_A_DETAIL *>(det);
399 QString url = QString::fromUtf8(utf8: detail->href.text, size: int(detail->href.size));
400 QString title = QString::fromUtf8(utf8: detail->title.text, size: int(detail->title.size));
401 charFmt.setAnchor(true);
402 charFmt.setAnchorHref(url);
403 if (!title.isEmpty())
404 charFmt.setToolTip(title);
405 charFmt.setForeground(m_palette.link());
406 qCDebug(lcMD) << "anchor" << url << title;
407 } break;
408 case MD_SPAN_IMG: {
409 m_imageSpan = true;
410 m_imageFormat = QTextImageFormat();
411 MD_SPAN_IMG_DETAIL *detail = static_cast<MD_SPAN_IMG_DETAIL *>(det);
412 m_imageFormat.setName(QString::fromUtf8(utf8: detail->src.text, size: int(detail->src.size)));
413 m_imageFormat.setProperty(propertyId: QTextFormat::ImageTitle, value: QString::fromUtf8(utf8: detail->title.text, size: int(detail->title.size)));
414 break;
415 }
416 case MD_SPAN_CODE:
417 charFmt.setFont(font: m_monoFont);
418 charFmt.setFontFixedPitch(true);
419 break;
420 case MD_SPAN_DEL:
421 charFmt.setFontStrikeOut(true);
422 break;
423 }
424 m_spanFormatStack.push(t: charFmt);
425 qCDebug(lcMD) << spanType << "setCharFormat" << charFmt.font().families().constFirst()
426 << charFmt.fontWeight() << (charFmt.fontItalic() ? "italic" : "")
427 << charFmt.foreground().color().name();
428 m_cursor.setCharFormat(charFmt);
429 return 0; // no error
430}
431
432int QTextMarkdownImporter::cbLeaveSpan(int spanType, void *detail)
433{
434 Q_UNUSED(detail);
435 QTextCharFormat charFmt;
436 if (!m_spanFormatStack.isEmpty()) {
437 m_spanFormatStack.pop();
438 if (!m_spanFormatStack.isEmpty())
439 charFmt = m_spanFormatStack.top();
440 }
441 m_cursor.setCharFormat(charFmt);
442 qCDebug(lcMD) << spanType << "setCharFormat" << charFmt.font().families().constFirst()
443 << charFmt.fontWeight() << (charFmt.fontItalic() ? "italic" : "")
444 << charFmt.foreground().color().name();
445 if (spanType == int(MD_SPAN_IMG))
446 m_imageSpan = false;
447 return 0; // no error
448}
449
450int QTextMarkdownImporter::cbText(int textType, const char *text, unsigned size)
451{
452 if (m_needsInsertBlock)
453 insertBlock();
454#if QT_CONFIG(regularexpression)
455 static const QRegularExpression openingBracket(QStringLiteral("<[a-zA-Z]"));
456 static const QRegularExpression closingBracket(QStringLiteral("(/>|</)"));
457#endif
458 QString s = QString::fromUtf8(utf8: text, size: int(size));
459
460 switch (textType) {
461 case MD_TEXT_NORMAL:
462#if QT_CONFIG(regularexpression)
463 if (m_htmlTagDepth) {
464 m_htmlAccumulator += s;
465 s = QString();
466 }
467#endif
468 break;
469 case MD_TEXT_NULLCHAR:
470 s = QString(QChar(u'\xFFFD')); // CommonMark-required replacement for null
471 break;
472 case MD_TEXT_BR:
473 s = QString(qtmi_Newline);
474 break;
475 case MD_TEXT_SOFTBR:
476 s = QString(qtmi_Space);
477 break;
478 case MD_TEXT_CODE:
479 // We'll see MD_SPAN_CODE too, which will set the char format, and that's enough.
480 break;
481#if QT_CONFIG(texthtmlparser)
482 case MD_TEXT_ENTITY:
483 if (m_htmlTagDepth)
484 m_htmlAccumulator += s;
485 else
486 m_cursor.insertHtml(html: s);
487 s = QString();
488 break;
489#endif
490 case MD_TEXT_HTML:
491 // count how many tags are opened and how many are closed
492#if QT_CONFIG(regularexpression) && QT_CONFIG(texthtmlparser)
493 {
494 int startIdx = 0;
495 while ((startIdx = s.indexOf(re: openingBracket, from: startIdx)) >= 0) {
496 ++m_htmlTagDepth;
497 startIdx += 2;
498 }
499 startIdx = 0;
500 while ((startIdx = s.indexOf(re: closingBracket, from: startIdx)) >= 0) {
501 --m_htmlTagDepth;
502 startIdx += 2;
503 }
504 }
505 m_htmlAccumulator += s;
506 if (!m_htmlTagDepth) { // all open tags are now closed
507 qCDebug(lcMD) << "HTML" << m_htmlAccumulator;
508 m_cursor.insertHtml(html: m_htmlAccumulator);
509 if (m_spanFormatStack.isEmpty())
510 m_cursor.setCharFormat(QTextCharFormat());
511 else
512 m_cursor.setCharFormat(m_spanFormatStack.top());
513 m_htmlAccumulator = QString();
514 }
515#endif
516 s = QString();
517 break;
518 }
519
520 switch (m_blockType) {
521 case MD_BLOCK_TD:
522 m_nonEmptyTableCells.append(t: m_tableCol);
523 break;
524 case MD_BLOCK_CODE:
525 if (s == qtmi_Newline) {
526 // defer a blank line until we see something else in the code block,
527 // to avoid ending every code block with a gratuitous blank line
528 m_needsInsertBlock = true;
529 s = QString();
530 }
531 break;
532 default:
533 break;
534 }
535
536 if (m_imageSpan) {
537 // TODO we don't yet support alt text with formatting, because of the cases where m_cursor
538 // already inserted the text above. Rather need to accumulate it in case we need it here.
539 m_imageFormat.setProperty(propertyId: QTextFormat::ImageAltText, value: s);
540 qCDebug(lcMD) << "image" << m_imageFormat.name()
541 << "title" << m_imageFormat.stringProperty(propertyId: QTextFormat::ImageTitle)
542 << "alt" << s << "relative to" << m_cursor.document()->baseUrl();
543 m_cursor.insertImage(format: m_imageFormat);
544 return 0; // no error
545 }
546
547 if (!s.isEmpty())
548 m_cursor.insertText(text: s);
549 if (m_cursor.currentList()) {
550 // The list item will indent the list item's text, so we don't need indentation on the block.
551 QTextBlockFormat bfmt = m_cursor.blockFormat();
552 bfmt.setIndent(0);
553 m_cursor.setBlockFormat(bfmt);
554 }
555 if (lcMD().isEnabled(type: QtDebugMsg)) {
556 QTextBlockFormat bfmt = m_cursor.blockFormat();
557 QString debugInfo;
558 if (m_cursor.currentList())
559 debugInfo = "in list at depth "_L1 + QString::number(m_cursor.currentList()->format().indent());
560 if (bfmt.hasProperty(propertyId: QTextFormat::BlockQuoteLevel))
561 debugInfo += "in blockquote at depth "_L1 +
562 QString::number(bfmt.intProperty(propertyId: QTextFormat::BlockQuoteLevel));
563 if (bfmt.hasProperty(propertyId: QTextFormat::BlockCodeLanguage))
564 debugInfo += "in a code block"_L1;
565 qCDebug(lcMD) << textType << "in block" << m_blockType << s << qPrintable(debugInfo)
566 << "bindent" << bfmt.indent() << "tindent" << bfmt.textIndent()
567 << "margins" << bfmt.leftMargin() << bfmt.topMargin() << bfmt.bottomMargin() << bfmt.rightMargin();
568 }
569 return 0; // no error
570}
571
572/*!
573 Insert a new block based on stored state.
574
575 m_cursor cannot store the state for the _next_ block ahead of time, because
576 m_cursor.setBlockFormat() controls the format of the block that the cursor
577 is already in; so cbLeaveBlock() cannot call setBlockFormat() without
578 altering the block that was just added. Therefore cbLeaveBlock() and the
579 following cbEnterBlock() set variables to remember what formatting should
580 come next, and insertBlock() is called just before the actual text
581 insertion, to create a new block with the right formatting.
582*/
583void QTextMarkdownImporter::insertBlock()
584{
585 QTextCharFormat charFormat;
586 if (!m_spanFormatStack.isEmpty())
587 charFormat = m_spanFormatStack.top();
588 QTextBlockFormat blockFormat;
589 if (!m_listStack.isEmpty() && !m_needsInsertList && m_listItem) {
590 QTextList *list = m_listStack.top();
591 if (list)
592 blockFormat = list->item(i: list->count() - 1).blockFormat();
593 else
594 qWarning() << "attempted to insert into a list that no longer exists";
595 }
596 if (m_blockQuoteDepth) {
597 blockFormat.setProperty(propertyId: QTextFormat::BlockQuoteLevel, value: m_blockQuoteDepth);
598 blockFormat.setLeftMargin(qtmi_BlockQuoteIndent * m_blockQuoteDepth);
599 blockFormat.setRightMargin(qtmi_BlockQuoteIndent);
600 }
601 if (m_codeBlock) {
602 blockFormat.setProperty(propertyId: QTextFormat::BlockCodeLanguage, value: m_blockCodeLanguage);
603 if (m_blockCodeFence) {
604 blockFormat.setNonBreakableLines(true);
605 blockFormat.setProperty(propertyId: QTextFormat::BlockCodeFence, value: QString(QLatin1Char(m_blockCodeFence)));
606 }
607 charFormat.setFont(font: m_monoFont);
608 } else {
609 blockFormat.setTopMargin(m_paragraphMargin);
610 blockFormat.setBottomMargin(m_paragraphMargin);
611 }
612 if (m_markerType == QTextBlockFormat::MarkerType::NoMarker)
613 blockFormat.clearProperty(propertyId: QTextFormat::BlockMarker);
614 else
615 blockFormat.setMarker(m_markerType);
616 if (!m_listStack.isEmpty())
617 blockFormat.setIndent(m_listStack.size());
618 if (m_cursor.document()->isEmpty()) {
619 m_cursor.setBlockFormat(blockFormat);
620 m_cursor.setCharFormat(charFormat);
621 } else if (m_listItem) {
622 m_cursor.insertBlock(format: blockFormat, charFormat: QTextCharFormat());
623 m_cursor.setCharFormat(charFormat);
624 } else {
625 m_cursor.insertBlock(format: blockFormat, charFormat);
626 }
627 if (m_needsInsertList) {
628 m_listStack.push(t: m_cursor.createList(format: m_listFormat));
629 } else if (!m_listStack.isEmpty() && m_listItem && m_listStack.top()) {
630 m_listStack.top()->add(block: m_cursor.block());
631 }
632 m_needsInsertList = false;
633 m_needsInsertBlock = false;
634}
635
636QT_END_NAMESPACE
637

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

source code of qtbase/src/gui/text/qtextmarkdownimporter.cpp