Coverage Report

Created: 2026-06-07 08:13

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