Coverage Report

Created: 2025-12-31 07:23

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