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