Coverage Report

Created: 2026-10-27 06:57

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/qtbase/src/gui/text/qtextmarkdownwriter.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
4
#include "qtextmarkdownwriter_p.h"
5
#include "qtextdocumentlayout_p.h"
6
#include "qfontinfo.h"
7
#include "qfontmetrics.h"
8
#include "qtextdocument_p.h"
9
#include "qtextlist.h"
10
#include "qtexttable.h"
11
#include "qtextcursor.h"
12
#include "qtextimagehandler_p.h"
13
#include "qtextmarkdownimporter_p.h"
14
#include "qloggingcategory.h"
15
#include <QtCore/QRegularExpression>
16
#if QT_CONFIG(itemmodel)
17
#include "qabstractitemmodel.h"
18
#endif
19
20
#include <utility>
21
22
QT_BEGIN_NAMESPACE
23
24
using namespace Qt::StringLiterals;
25
26
Q_STATIC_LOGGING_CATEGORY(lcMDW, "qt.text.markdown.writer")
27
28
static const QChar qtmw_Space = u' ';
29
static const QChar qtmw_Tab = u'\t';
30
static const QChar qtmw_Newline = u'\n';
31
static const QChar qtmw_CarriageReturn = u'\r';
32
static const QChar qtmw_LineBreak = u'\x2028';
33
static const QChar qtmw_DoubleQuote = u'"';
34
static const QChar qtmw_Backtick = u'`';
35
static const QChar qtmw_Backslash = u'\\';
36
static const QChar qtmw_Period = u'.';
37
38
QTextMarkdownWriter::QTextMarkdownWriter(QTextStream &stream, QTextDocument::MarkdownFeatures features)
39
0
  : m_stream(&stream), m_features(features)
40
0
{
41
0
}
42
43
bool QTextMarkdownWriter::writeAll(const QTextDocument *document)
44
0
{
45
0
    writeFrontMatter(document->metaInformation(QTextDocument::FrontMatter));
46
0
    writeFrame(document->rootFrame());
47
0
    return true;
48
0
}
49
50
#if QT_CONFIG(itemmodel)
51
void QTextMarkdownWriter::writeTable(const QAbstractItemModel *table)
52
0
{
53
0
    QList<int> tableColumnWidths(table->columnCount());
54
0
    for (int col = 0; col < table->columnCount(); ++col) {
55
0
        tableColumnWidths[col] = table->headerData(col, Qt::Horizontal).toString().size();
56
0
        for (int row = 0; row < table->rowCount(); ++row) {
57
0
            tableColumnWidths[col] = qMax(tableColumnWidths[col],
58
0
                table->data(table->index(row, col)).toString().size());
59
0
        }
60
0
    }
61
62
    // write the header and separator
63
0
    for (int col = 0; col < table->columnCount(); ++col) {
64
0
        QString s = table->headerData(col, Qt::Horizontal).toString();
65
0
        (*m_stream) << '|' << s << QString(tableColumnWidths[col] - s.size(), qtmw_Space);
66
0
    }
67
0
    (*m_stream) << "|" << Qt::endl;
68
0
    for (int col = 0; col < tableColumnWidths.size(); ++col)
69
0
        (*m_stream) << '|' << QString(tableColumnWidths[col], u'-');
70
0
    (*m_stream) << '|'<< Qt::endl;
71
72
    // write the body
73
0
    for (int row = 0; row < table->rowCount(); ++row) {
74
0
        for (int col = 0; col < table->columnCount(); ++col) {
75
0
            QString s = table->data(table->index(row, col)).toString();
76
0
            (*m_stream) << '|' << s << QString(tableColumnWidths[col] - s.size(), qtmw_Space);
77
0
        }
78
0
        (*m_stream) << '|'<< Qt::endl;
79
0
    }
80
0
    m_listInfo.clear();
81
0
}
82
#endif
83
84
void QTextMarkdownWriter::writeFrontMatter(const QString &fm)
85
0
{
86
0
    const bool featureEnabled = m_features.testFlag(
87
0
            static_cast<QTextDocument::MarkdownFeature>(QTextMarkdownImporter::FeatureFrontMatter));
88
0
    qCDebug(lcMDW) << "writing FrontMatter?" << featureEnabled << "size" << fm.size();
89
0
    if (fm.isEmpty() || !featureEnabled)
90
0
        return;
91
0
    (*m_stream) << "---\n"_L1 << fm;
92
0
    if (!fm.endsWith(qtmw_Newline))
93
0
        (*m_stream) << qtmw_Newline;
94
0
    (*m_stream) << "---\n"_L1;
95
0
}
96
97
/*! \internal
98
    Measure how wide \a cell will be when written out, including the markup that
99
    writeBlock() will add: emphasis and code-span markers, backslash escapes and
100
    so on. Pass \a ignoreFormat exactly as it will be passed when writing the
101
    cell for real, so that the measurement matches.
102
103
    The only reliable way to know the width is to write the cell to a scratch
104
    stream and see how long the result is; so we temporarily redirect m_stream,
105
    and restore the writer's state afterwards, because this is only a
106
    measurement and must not affect the real output.
107
*/
108
int QTextMarkdownWriter::measureCellWidth(const QTextTableCell &cell, bool ignoreFormat)
109
0
{
110
0
    QString scratch;
111
0
    QTextStream scratchStream(&scratch);
112
113
    // Measuring runs writeBlock() for its return value only. That mutates the writer's
114
    // state and writes to the stream, so swap in a scratch stream and pristine state,
115
    // then restore everything. That makes the measurement accurate, since it matches
116
    // the state writeBlock() will be called with when the cell is actually written.
117
0
    QScopedValueRollback stateRollback(m_blockState, BlockState{});
118
0
    QScopedValueRollback streamRollback(m_stream, &scratchStream);
119
120
0
    int width = 0;
121
0
    for (auto it = cell.begin(); it != cell.end(); ++it) {
122
0
        const QTextBlock block = it.currentBlock();
123
0
        if (block.isValid())
124
0
            width += writeBlock(block, false, ignoreFormat, true);
125
0
    }
126
127
0
    return width;
128
0
}
129
130
void QTextMarkdownWriter::writeFrame(const QTextFrame *frame)
131
0
{
132
0
    Q_ASSERT(frame);
133
0
    const QTextTable *table = qobject_cast<const QTextTable*> (frame);
134
0
    QTextFrame::iterator iterator = frame->begin();
135
0
    QTextFrame *child = nullptr;
136
0
    int tableRow = -1;
137
0
    bool lastWasList = false;
138
0
    QList<int> tableColumnWidths;
139
0
    if (table) {
140
0
        tableColumnWidths.resize(table->columns());
141
0
        for (int col = 0; col < table->columns(); ++col) {
142
0
            for (int row = 0; row < table->rows(); ++ row) {
143
0
                QTextTableCell cell = table->cellAt(row, col);
144
0
                if (cell.columnSpan() != 1)
145
0
                    continue;
146
                // Row 0 is written as the header, with character formatting ignored
147
                // (see the ignoreFormat argument to writeBlock() below), so measure
148
                // it the same way; otherwise we'd reserve room for markup that
149
                // never gets written.
150
0
                const int cellTextLen = measureCellWidth(cell, row == 0);
151
0
                if (tableColumnWidths[col] < cellTextLen)
152
0
                    tableColumnWidths[col] = cellTextLen;
153
0
            }
154
0
        }
155
0
    }
156
0
    while (!iterator.atEnd()) {
157
0
        if (iterator.currentFrame() && child != iterator.currentFrame())
158
0
            writeFrame(iterator.currentFrame());
159
0
        else { // no frame, it's a block
160
0
            QTextBlock block = iterator.currentBlock();
161
            // Look ahead and detect some cases when we should
162
            // suppress needless blank lines, when there will be a big change in block format
163
0
            bool nextIsDifferent = false;
164
0
            bool ending = false;
165
0
            int blockQuoteIndent = 0;
166
0
            int nextBlockQuoteIndent = 0;
167
0
            {
168
0
                QTextFrame::iterator next = iterator;
169
0
                ++next;
170
0
                QTextBlockFormat format = iterator.currentBlock().blockFormat();
171
0
                QTextBlockFormat nextFormat = next.currentBlock().blockFormat();
172
0
                blockQuoteIndent = format.intProperty(QTextFormat::BlockQuoteLevel);
173
0
                nextBlockQuoteIndent = nextFormat.intProperty(QTextFormat::BlockQuoteLevel);
174
0
                if (next.atEnd()) {
175
0
                    nextIsDifferent = true;
176
0
                    ending = true;
177
0
                } else {
178
0
                    if (nextFormat.indent() != format.indent() ||
179
0
                        nextFormat.property(QTextFormat::BlockCodeLanguage) !=
180
0
                                format.property(QTextFormat::BlockCodeLanguage))
181
0
                        nextIsDifferent = true;
182
0
                }
183
0
            }
184
0
            if (table) {
185
0
                QTextTableCell cell = table->cellAt(block.position());
186
0
                if (tableRow < cell.row()) {
187
0
                    if (tableRow == 0) {
188
0
                        (*m_stream) << qtmw_Newline;
189
0
                        for (int col = 0; col < tableColumnWidths.size(); ++col)
190
0
                            (*m_stream) << '|' << QString(tableColumnWidths[col], u'-');
191
0
                        (*m_stream) << '|';
192
0
                    }
193
0
                    (*m_stream) << qtmw_Newline << '|';
194
0
                    tableRow = cell.row();
195
0
                }
196
0
            } else if (!block.textList()) {
197
0
                if (lastWasList) {
198
0
                    (*m_stream) << qtmw_Newline;
199
0
                    m_blockState.linePrefixWritten = false;
200
0
                }
201
0
            }
202
0
            int endingCol = writeBlock(block, !table, table && tableRow == 0,
203
0
                                       nextIsDifferent && !block.textList());
204
0
            m_blockState.doubleNewlineWritten = false;
205
0
            if (table) {
206
0
                QTextTableCell cell = table->cellAt(block.position());
207
0
                int paddingLen = -endingCol;
208
0
                int spanEndCol = cell.column() + cell.columnSpan();
209
0
                for (int col = cell.column(); col < spanEndCol; ++col)
210
0
                    paddingLen += tableColumnWidths[col];
211
0
                if (paddingLen > 0)
212
0
                    (*m_stream) << QString(paddingLen, qtmw_Space);
213
0
                for (int col = cell.column(); col < spanEndCol; ++col)
214
0
                    (*m_stream) << "|";
215
0
            } else if (m_blockState.fencedCodeBlock && ending) {
216
0
                (*m_stream) << qtmw_Newline << m_blockState.linePrefix << QString(m_blockState.wrappedLineIndent, qtmw_Space)
217
0
                         << m_blockState.codeBlockFence << qtmw_Newline << qtmw_Newline;
218
0
                m_blockState.codeBlockFence.clear();
219
0
            } else if (m_blockState.indentedCodeBlock && nextIsDifferent) {
220
0
                (*m_stream) << qtmw_Newline << qtmw_Newline;
221
0
            } else if (endingCol > 0) {
222
0
                if (block.textList() || block.blockFormat().hasProperty(QTextFormat::BlockCodeLanguage)) {
223
0
                    (*m_stream) << qtmw_Newline;
224
                    // list inside a block quote: carry the "> " prefix onto the next line
225
                    // quote nested inside a list item: don't end with a dangling  "> "
226
0
                    const bool quoteEndsWithItem = block.blockFormat().boolProperty(
227
0
                            QTextFormat::BlockQuoteInsideListItem);
228
0
                    if (block.textList() && !quoteEndsWithItem) {
229
0
                        (*m_stream) << m_blockState.linePrefix;
230
0
                        m_blockState.linePrefixWritten = true;
231
0
                    }
232
0
                } else {
233
0
                    (*m_stream) << qtmw_Newline;
234
0
                    if (nextBlockQuoteIndent < blockQuoteIndent)
235
0
                        setLinePrefixForBlockQuote(nextBlockQuoteIndent);
236
0
                    (*m_stream) << m_blockState.linePrefix;
237
0
                    (*m_stream) << qtmw_Newline;
238
0
                    m_blockState.doubleNewlineWritten = true;
239
0
                }
240
0
            }
241
0
            lastWasList = block.textList();
242
0
        }
243
0
        child = iterator.currentFrame();
244
0
        ++iterator;
245
0
    }
246
0
    if (table) {
247
0
        (*m_stream) << qtmw_Newline << qtmw_Newline;
248
0
        m_blockState.doubleNewlineWritten = true;
249
0
    }
250
0
    m_listInfo.clear();
251
0
}
252
253
QTextMarkdownWriter::ListInfo QTextMarkdownWriter::listInfo(QTextList *list)
254
0
{
255
0
    if (!m_listInfo.contains(list)) {
256
        // decide whether this list is loose or tight
257
0
        ListInfo info;
258
0
        info.loose = false;
259
0
        if (list->count() > 1) {
260
0
            QTextBlock first = list->item(0);
261
0
            QTextBlock last = list->item(list->count() - 1);
262
0
            QTextBlock next = first.next();
263
0
            while (next.isValid()) {
264
0
                if (next == last)
265
0
                    break;
266
0
                qCDebug(lcMDW) << "next block in list" << list << next.text() << "part of list?" << next.textList();
267
0
                if (!next.textList()) {
268
                    // If we find a continuation paragraph, this list is "loose"
269
                    // because it will need a blank line to separate that paragraph.
270
0
                    qCDebug(lcMDW) << "decided list beginning with" << first.text() << "is loose after" << next.text();
271
0
                    info.loose = true;
272
0
                    break;
273
0
                }
274
0
                next = next.next();
275
0
            }
276
0
        }
277
0
        m_listInfo.insert(list, info);
278
0
        return info;
279
0
    }
280
0
    return m_listInfo.value(list);
281
0
}
282
283
void QTextMarkdownWriter::setLinePrefixForBlockQuote(int level)
284
0
{
285
0
    m_blockState.linePrefix.clear();
286
0
    if (level > 0) {
287
0
        m_blockState.linePrefix.reserve(level * 2);
288
0
        for (int i = 0; i < level; ++i)
289
0
            m_blockState.linePrefix += u"> ";
290
0
    }
291
0
}
292
293
static int nearestWordWrapIndex(const QString &s, int before)
294
0
{
295
0
    before = qMin(before, s.size());
296
0
    int fragBegin = qMax(before - 15, 0);
297
0
    if (lcMDW().isDebugEnabled()) {
298
0
        QString frag = s.mid(fragBegin, 30);
299
0
        qCDebug(lcMDW) << frag << before;
300
0
        qCDebug(lcMDW) << QString(before - fragBegin, qtmw_Period) + u'<';
301
0
    }
302
0
    for (int i = before - 1; i >= 0; --i) {
303
0
        if (s.at(i).isSpace()) {
304
0
            qCDebug(lcMDW) << QString(i - fragBegin, qtmw_Period) + u'^' << i;
305
0
            return i;
306
0
        }
307
0
    }
308
0
    qCDebug(lcMDW, "not possible");
309
0
    return -1;
310
0
}
311
312
static int adjacentBackticksCount(const QString &s)
313
0
{
314
0
    int start = -1, len = s.size();
315
0
    int ret = 0;
316
0
    for (int i = 0; i < len; ++i) {
317
0
        if (s.at(i) == qtmw_Backtick) {
318
0
            if (start < 0)
319
0
                start = i;
320
0
        } else if (start >= 0) {
321
0
            ret = qMax(ret, i - start);
322
0
            start = -1;
323
0
        }
324
0
    }
325
0
    if (s.at(len - 1) == qtmw_Backtick)
326
0
        ret = qMax(ret, len - start);
327
0
    return ret;
328
0
}
329
330
/*! \internal
331
    Escape anything at the beginning of a line of markdown that would be
332
    misinterpreted by a markdown parser, including any period that follows a
333
    number (to avoid misinterpretation as a numbered list item).
334
    https://spec.commonmark.org/0.31.2/#backslash-escapes
335
*/
336
static void maybeEscapeFirstChar(QString &s)
337
0
{
338
0
    static const QRegularExpression numericListRe(uR"(\d+([\.)])\s)"_s);
339
0
    constexpr auto specialFirstCharacters = "#*+-"_L1;
340
341
0
    QString sTrimmed = s.trimmed();
342
0
    if (sTrimmed.isEmpty())
343
0
        return;
344
0
    QChar firstChar = sTrimmed.at(0);
345
0
    if (specialFirstCharacters.contains(firstChar)) {
346
0
        int i = s.indexOf(firstChar); // == 0 unless s got trimmed
347
0
        s.insert(i, u'\\');
348
0
    } else {
349
0
        auto match = numericListRe.match(s, 0, QRegularExpression::NormalMatch,
350
0
                                         QRegularExpression::AnchorAtOffsetMatchOption);
351
0
        if (match.hasMatch())
352
0
            s.insert(match.capturedStart(1), qtmw_Backslash);
353
0
    }
354
0
}
355
356
/*! \internal
357
    Escape all backslashes. Then escape any special character that stands
358
    alone or prefixes a "word", including the \c < that starts an HTML tag.
359
    https://spec.commonmark.org/0.31.2/#backslash-escapes
360
*/
361
static void escapeSpecialCharacters(QString &s)
362
0
{
363
0
    static const QRegularExpression spaceRe(uR"(\s+)"_s);
364
0
    static const QRegularExpression specialRe(uR"([<!*[`&]+[/\w])"_s);
365
366
0
    s.replace("\\"_L1, "\\\\"_L1);
367
368
0
    int i = 0;
369
0
    while (i >= 0) {
370
0
        if (int j = s.indexOf(specialRe, i); j >= 0) {
371
0
            s.insert(j, qtmw_Backslash);
372
0
            i = j + 3;
373
0
        }
374
0
        i = s.indexOf(spaceRe, i);
375
0
        if (i >= 0)
376
0
            ++i; // past the whitespace, if found
377
0
    }
378
0
}
379
380
struct LineEndPositions {
381
    const QChar *lineEnd;
382
    const QChar *nextLineBegin;
383
};
384
385
static LineEndPositions findLineEnd(const QChar *begin, const QChar *end)
386
0
{
387
0
    LineEndPositions result{ end, end };
388
389
0
    while (begin < end) {
390
0
        if (*begin == qtmw_Newline) {
391
0
            result.lineEnd = begin;
392
0
            result.nextLineBegin = begin + 1;
393
0
            break;
394
0
        } else if (*begin == qtmw_CarriageReturn) {
395
0
            result.lineEnd = begin;
396
0
            result.nextLineBegin = begin + 1;
397
0
            if (((begin + 1) < end) && begin[1] == qtmw_Newline)
398
0
                ++result.nextLineBegin;
399
0
            break;
400
0
        }
401
402
0
        ++begin;
403
0
    }
404
405
0
    return result;
406
0
}
407
408
static bool isBlankLine(const QChar *begin, const QChar *end)
409
0
{
410
0
    while (begin < end) {
411
0
        if (*begin != qtmw_Space && *begin != qtmw_Tab)
412
0
            return false;
413
0
        ++begin;
414
0
    }
415
0
    return true;
416
0
}
417
418
static QString createLinkTitle(const QString &title)
419
0
{
420
0
    QString result;
421
0
    result.reserve(title.size() + 2);
422
0
    result += qtmw_DoubleQuote;
423
424
0
    const QChar *data = title.data();
425
0
    const QChar *end = data + title.size();
426
427
0
    while (data < end) {
428
0
        const auto lineEndPositions = findLineEnd(data, end);
429
430
0
        if (!isBlankLine(data, lineEndPositions.lineEnd)) {
431
0
            while (data < lineEndPositions.nextLineBegin) {
432
0
                if (*data == qtmw_DoubleQuote)
433
0
                    result += qtmw_Backslash;
434
0
                result += *data;
435
0
                ++data;
436
0
            }
437
0
        }
438
439
0
        data = lineEndPositions.nextLineBegin;
440
0
    }
441
442
0
    result += qtmw_DoubleQuote;
443
0
    return result;
444
0
}
445
446
int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ignoreFormat, bool ignoreEmpty)
447
0
{
448
0
    if (block.text().isEmpty() && ignoreEmpty)
449
0
        return 0;
450
0
    const int ColumnLimit = 80;
451
0
    QTextBlockFormat blockFmt = block.blockFormat();
452
0
    bool missedBlankCodeBlockLine = false;
453
0
    const bool codeBlock = blockFmt.hasProperty(QTextFormat::BlockCodeFence) ||
454
0
            blockFmt.stringProperty(QTextFormat::BlockCodeLanguage).size() > 0 ||
455
0
            blockFmt.nonBreakableLines();
456
0
    const int blockQuoteLevel = blockFmt.intProperty(QTextFormat::BlockQuoteLevel);
457
0
    if (m_blockState.fencedCodeBlock && !codeBlock) {
458
0
        (*m_stream) << m_blockState.linePrefix << m_blockState.codeBlockFence << qtmw_Newline;
459
0
        m_blockState.fencedCodeBlock = false;
460
0
        m_blockState.codeBlockFence.clear();
461
0
        m_blockState.linePrefixWritten = m_blockState.linePrefix.size() > 0;
462
0
    }
463
0
    m_blockState.linePrefix.clear();
464
    // block inside a list item: "- > quoted"; leave the prefix to append below
465
    // list inside a block quote: "> - quoted"; nothing special here
466
0
    const bool quoteInListItem = blockQuoteLevel > 0 && block.textList()
467
0
            && blockFmt.boolProperty(QTextFormat::BlockQuoteInsideListItem);
468
0
    if (!blockFmt.headingLevel() && blockQuoteLevel > 0) {
469
0
        setLinePrefixForBlockQuote(blockQuoteLevel);
470
0
        if (!m_blockState.linePrefixWritten && !quoteInListItem) {
471
0
            (*m_stream) << m_blockState.linePrefix;
472
0
            m_blockState.linePrefixWritten = true;
473
0
        }
474
0
    }
475
0
    if (block.textList()) { // it's a list-item
476
0
        auto fmt = block.textList()->format();
477
0
        const int listLevel = fmt.indent();
478
        // Negative numbers don't start a list in Markdown, so ignore them.
479
0
        const int start = fmt.start() >= 0 ? fmt.start() : 1;
480
0
        const int number = block.textList()->itemNumber(block) + start;
481
0
        QByteArray bullet = " ";
482
0
        bool numeric = false;
483
0
        switch (fmt.style()) {
484
0
        case QTextListFormat::ListDisc:
485
0
            bullet = "-";
486
0
            m_blockState.wrappedLineIndent = 2;
487
0
            break;
488
0
        case QTextListFormat::ListCircle:
489
0
            bullet = "*";
490
0
            m_blockState.wrappedLineIndent = 2;
491
0
            break;
492
0
        case QTextListFormat::ListSquare:
493
0
            bullet = "+";
494
0
            m_blockState.wrappedLineIndent = 2;
495
0
            break;
496
0
        case QTextListFormat::ListStyleUndefined: break;
497
0
        case QTextListFormat::ListDecimal:
498
0
        case QTextListFormat::ListLowerAlpha:
499
0
        case QTextListFormat::ListUpperAlpha:
500
0
        case QTextListFormat::ListLowerRoman:
501
0
        case QTextListFormat::ListUpperRoman:
502
0
            numeric = true;
503
0
            m_blockState.wrappedLineIndent = 4;
504
0
            break;
505
0
        }
506
0
        switch (blockFmt.marker()) {
507
0
        case QTextBlockFormat::MarkerType::Checked:
508
0
            bullet += " [x]";
509
0
            break;
510
0
        case QTextBlockFormat::MarkerType::Unchecked:
511
0
            bullet += " [ ]";
512
0
            break;
513
0
        default:
514
0
            break;
515
0
        }
516
0
        int indentFirstLine = (listLevel - 1) * (numeric ? 4 : 2);
517
0
        m_blockState.wrappedLineIndent += indentFirstLine;
518
        // An ordered list can interrupt a paragraph only if it starts with 1:
519
        // https://spec.commonmark.org/0.31.2/#list-items
520
        // So an indented "2." resuming a deeper list after a shallower item
521
        // is read back as a lazy continuation line of that shallower item,
522
        // smashing the two items together. A blank line in between prevents that.
523
        // Bullets can always interrupt, so they need no help.
524
0
        const bool cannotInterruptParagraph =
525
0
                numeric && number != 1 && listLevel > m_blockState.lastListIndent;
526
0
        if (m_blockState.lastListIndent != listLevel && !m_blockState.doubleNewlineWritten
527
0
                && (cannotInterruptParagraph || listInfo(block.textList()).loose))
528
0
            (*m_stream) << qtmw_Newline;
529
0
        m_blockState.lastListIndent = listLevel;
530
0
        QString prefix(indentFirstLine, qtmw_Space);
531
0
        if (numeric) {
532
0
            QString suffix = fmt.numberSuffix();
533
0
            if (suffix.isEmpty())
534
0
                suffix = QString(qtmw_Period);
535
0
            QString numberStr = QString::number(number) + suffix + qtmw_Space;
536
0
            if (numberStr.size() == 3)
537
0
                numberStr += qtmw_Space;
538
0
            prefix += numberStr;
539
0
        } else {
540
0
            prefix += QLatin1StringView(bullet) + qtmw_Space;
541
0
        }
542
0
        if (quoteInListItem) {
543
            // "- > ": quote nested inside list item
544
0
            prefix += m_blockState.linePrefix;
545
0
            m_blockState.linePrefixWritten = true;
546
0
        }
547
0
        (*m_stream) << prefix;
548
0
    } else if (blockFmt.hasProperty(QTextFormat::BlockTrailingHorizontalRulerWidth)) {
549
0
        (*m_stream) << "- - -\n"; // unambiguous horizontal rule, not an underline under a heading
550
0
        return 0;
551
0
    } else if (codeBlock) {
552
        // It's important to preserve blank lines in code blocks.  But blank lines in code blocks
553
        // inside block quotes are getting preserved anyway (along with the "> " prefix).
554
0
        if (!blockFmt.hasProperty(QTextFormat::BlockQuoteLevel))
555
0
            missedBlankCodeBlockLine = true; // only if we don't get any fragments below
556
0
        if (!m_blockState.fencedCodeBlock) {
557
0
            QString fenceChar = blockFmt.stringProperty(QTextFormat::BlockCodeFence);
558
0
            if (fenceChar.isEmpty())
559
0
                fenceChar = "`"_L1;
560
0
            m_blockState.codeBlockFence = QString(3, fenceChar.at(0));
561
0
            if (blockFmt.hasProperty(QTextFormat::BlockIndent))
562
0
                m_blockState.codeBlockFence = QString(m_blockState.wrappedLineIndent, qtmw_Space) + m_blockState.codeBlockFence;
563
            // A block quote can contain an indented code block, but not vice-versa.
564
0
            (*m_stream) << m_blockState.codeBlockFence << blockFmt.stringProperty(QTextFormat::BlockCodeLanguage)
565
0
                     << qtmw_Newline << m_blockState.linePrefix;
566
0
            m_blockState.fencedCodeBlock = true;
567
0
        }
568
0
        wrap = false;
569
0
    } else if (!blockFmt.indent()) {
570
0
        m_blockState.wrappedLineIndent = 0;
571
0
        if (blockFmt.hasProperty(QTextFormat::BlockCodeLanguage)) {
572
            // A block quote can contain an indented code block, but not vice-versa.
573
0
            m_blockState.linePrefix += QString(4, qtmw_Space);
574
0
            m_blockState.indentedCodeBlock = true;
575
0
        }
576
0
        if (!m_blockState.linePrefixWritten) {
577
0
            (*m_stream) << m_blockState.linePrefix;
578
0
            m_blockState.linePrefixWritten = true;
579
0
        }
580
0
    }
581
0
    if (blockFmt.headingLevel()) {
582
0
        (*m_stream) << QByteArray(blockFmt.headingLevel(), '#') << ' ';
583
0
        wrap = false;
584
0
    }
585
586
    // Continuation lines of a quoted list item are indented to line up under the
587
    // item's text, and the quote marker goes after that indent: "  > continued".
588
0
    const QString wrappedLineIndentString(m_blockState.wrappedLineIndent, qtmw_Space);
589
0
    QString wrapIndentString;
590
0
    if (quoteInListItem)
591
0
        wrapIndentString = wrappedLineIndentString + m_blockState.linePrefix;
592
0
    else
593
0
        wrapIndentString = m_blockState.linePrefix + wrappedLineIndentString;
594
    // It would be convenient if QTextStream had a lineCharPos() accessor,
595
    // to keep track of how many characters (not bytes) have been written on the current line,
596
    // but it doesn't.  So we have to keep track with this col variable.
597
0
    int col = wrapIndentString.size();
598
0
    bool mono = false;
599
0
    bool startsOrEndsWithBacktick = false;
600
0
    bool bold = false;
601
0
    bool italic = false;
602
0
    bool underline = false;
603
0
    bool strikeOut = false;
604
0
    bool endingMarkers = false;
605
0
    QString backticks(qtmw_Backtick);
606
0
    for (QTextBlock::Iterator frag = block.begin(); !frag.atEnd(); ++frag) {
607
0
        missedBlankCodeBlockLine = false;
608
0
        QString fragmentText = frag.fragment().text();
609
0
        while (fragmentText.endsWith(qtmw_Newline))
610
0
            fragmentText.chop(1);
611
0
        if (!(m_blockState.fencedCodeBlock || m_blockState.indentedCodeBlock)) {
612
0
            escapeSpecialCharacters(fragmentText);
613
0
            maybeEscapeFirstChar(fragmentText);
614
0
        }
615
0
        if (block.textList()) { // <li>first line</br>continuation</li>
616
0
            QString newlineIndent =
617
0
                    QString(qtmw_Newline) + QString(m_blockState.wrappedLineIndent, qtmw_Space);
618
0
            fragmentText.replace(QString(qtmw_LineBreak), newlineIndent);
619
0
        } else if (blockFmt.indent() > 0) { // <li>first line<p>continuation</p></li>
620
0
            (*m_stream) << QString(m_blockState.wrappedLineIndent, qtmw_Space);
621
0
        } else {
622
0
            fragmentText.replace(qtmw_LineBreak, qtmw_Newline);
623
0
        }
624
0
        startsOrEndsWithBacktick |=
625
0
                fragmentText.startsWith(qtmw_Backtick) || fragmentText.endsWith(qtmw_Backtick);
626
0
        QTextCharFormat fmt = frag.fragment().charFormat();
627
0
        if (fmt.isImageFormat()) {
628
0
            QTextImageFormat ifmt = fmt.toImageFormat();
629
0
            QString desc = ifmt.stringProperty(QTextFormat::ImageAltText);
630
0
            if (desc.isEmpty())
631
0
                desc = "image"_L1;
632
0
            QString s = "!["_L1 + desc + "]("_L1 + ifmt.name();
633
0
            QString title = ifmt.stringProperty(QTextFormat::ImageTitle);
634
0
            if (!title.isEmpty())
635
0
                s += qtmw_Space + qtmw_DoubleQuote + title + qtmw_DoubleQuote;
636
0
            s += u')';
637
0
            if (wrap && col + s.size() > ColumnLimit) {
638
0
                (*m_stream) << qtmw_Newline << wrapIndentString;
639
0
                col = m_blockState.wrappedLineIndent;
640
0
            }
641
0
            (*m_stream) << s;
642
0
            col += s.size();
643
0
        } else if (fmt.hasProperty(QTextFormat::AnchorHref)) {
644
0
            const auto href = fmt.property(QTextFormat::AnchorHref).toString();
645
0
            const bool hasToolTip = fmt.hasProperty(QTextFormat::TextToolTip);
646
0
            QString s;
647
0
            if (!hasToolTip && href == fragmentText && !QUrl(href, QUrl::StrictMode).scheme().isEmpty()) {
648
0
                s = u'<' + href + u'>';
649
0
            } else {
650
0
                s = u'[' + fragmentText + "]("_L1 + href;
651
0
                if (hasToolTip) {
652
0
                    s += qtmw_Space;
653
0
                    s += createLinkTitle(fmt.property(QTextFormat::TextToolTip).toString());
654
0
                }
655
0
                s += u')';
656
0
            }
657
0
            if (wrap && col + s.size() > ColumnLimit) {
658
0
                (*m_stream) << qtmw_Newline << wrapIndentString;
659
0
                col = m_blockState.wrappedLineIndent;
660
0
            }
661
0
            (*m_stream) << s;
662
0
            col += s.size();
663
0
        } else {
664
0
            QFontInfo fontInfo(fmt.font());
665
0
            bool monoFrag = fontInfo.fixedPitch() || fmt.fontFixedPitch();
666
0
            QString markers;
667
0
            if (!ignoreFormat) {
668
0
                if (monoFrag != mono && !m_blockState.indentedCodeBlock && !m_blockState.fencedCodeBlock) {
669
0
                    if (monoFrag)
670
0
                        backticks =
671
0
                                QString(adjacentBackticksCount(fragmentText) + 1, qtmw_Backtick);
672
0
                    markers += backticks;
673
0
                    if (startsOrEndsWithBacktick)
674
0
                        markers += qtmw_Space;
675
0
                    mono = monoFrag;
676
0
                    if (!mono)
677
0
                        endingMarkers = true;
678
0
                }
679
0
                if (!blockFmt.headingLevel() && !mono) {
680
0
                    if (fontInfo.bold() != bold) {
681
0
                        markers += "**"_L1;
682
0
                        bold = fontInfo.bold();
683
0
                        if (!bold)
684
0
                            endingMarkers = true;
685
0
                    }
686
0
                    if (fontInfo.italic() != italic) {
687
0
                        markers += u'*';
688
0
                        italic = fontInfo.italic();
689
0
                        if (!italic)
690
0
                            endingMarkers = true;
691
0
                    }
692
0
                    if (fontInfo.strikeOut() != strikeOut) {
693
0
                        markers += "~~"_L1;
694
0
                        strikeOut = fontInfo.strikeOut();
695
0
                        if (!strikeOut)
696
0
                            endingMarkers = true;
697
0
                    }
698
0
                    if (fontInfo.underline() != underline) {
699
                        // CommonMark specifies underline as another way to get emphasis (italics):
700
                        // https://spec.commonmark.org/0.31.2/#example-148
701
                        // but md4c allows us to distinguish them; so we support underlining (in GitHub dialect).
702
0
                        markers += u'_';
703
0
                        underline = fontInfo.underline();
704
0
                        if (!underline)
705
0
                            endingMarkers = true;
706
0
                    }
707
0
                }
708
0
            }
709
0
            if (wrap && col + markers.size() * 2 + fragmentText.size() > ColumnLimit) {
710
0
                int i = 0;
711
0
                const int fragLen = fragmentText.size();
712
0
                bool breakingLine = false;
713
0
                while (i < fragLen) {
714
0
                    if (col >= ColumnLimit) {
715
                        // Markers that end a span belong on this line, next to the text they format;
716
                        // markers that begin a span should go the next line.
717
0
                        if (endingMarkers) {
718
0
                            (*m_stream) << markers;
719
0
                            markers.clear();
720
0
                        }
721
0
                        (*m_stream) << qtmw_Newline << wrapIndentString;
722
0
                        col = m_blockState.wrappedLineIndent;
723
0
                        while (i < fragLen && fragmentText[i].isSpace())
724
0
                            ++i;
725
0
                    }
726
0
                    int j = i + ColumnLimit - col;
727
0
                    if (j < fragLen) {
728
0
                        int wi = nearestWordWrapIndex(fragmentText, j);
729
0
                        if (wi < 0) {
730
0
                            j = fragLen;
731
                            // can't break within the fragment: we need to break already _before_ it
732
0
                            if (endingMarkers) {
733
0
                                (*m_stream) << markers;
734
0
                                markers.clear();
735
0
                            }
736
0
                            (*m_stream) << qtmw_Newline << wrapIndentString;
737
0
                            col = m_blockState.wrappedLineIndent;
738
0
                        } else if (wi >= i) {
739
0
                            j = wi;
740
0
                            breakingLine = true;
741
0
                        }
742
0
                    } else {
743
0
                        j = fragLen;
744
0
                        breakingLine = false;
745
0
                    }
746
0
                    QString subfrag = fragmentText.mid(i, j - i);
747
0
                    if (!i) {
748
0
                        (*m_stream) << markers;
749
0
                        col += markers.size();
750
0
                    }
751
0
                    if (col == m_blockState.wrappedLineIndent)
752
0
                        maybeEscapeFirstChar(subfrag);
753
0
                    (*m_stream) << subfrag;
754
0
                    if (breakingLine) {
755
0
                        (*m_stream) << qtmw_Newline << wrapIndentString;
756
0
                        col = m_blockState.wrappedLineIndent;
757
0
                    } else {
758
0
                        col += subfrag.size();
759
0
                    }
760
0
                    i = j + 1;
761
0
                } // loop over fragment characters (we know we need to break somewhere)
762
0
            } else {
763
0
                if (!m_blockState.linePrefixWritten && col == wrapIndentString.size()) {
764
0
                    (*m_stream) << m_blockState.linePrefix;
765
0
                    col += m_blockState.linePrefix.size();
766
0
                }
767
0
                (*m_stream) << markers << fragmentText;
768
0
                col += markers.size() + fragmentText.size();
769
0
            }
770
0
        }
771
0
    }
772
0
    if (mono) {
773
0
        if (startsOrEndsWithBacktick) {
774
0
            (*m_stream) << qtmw_Space;
775
0
            col += 1;
776
0
        }
777
0
        (*m_stream) << backticks;
778
0
        col += backticks.size();
779
0
    }
780
0
    if (bold) {
781
0
        (*m_stream) << "**";
782
0
        col += 2;
783
0
    }
784
0
    if (italic) {
785
0
        (*m_stream) << "*";
786
0
        col += 1;
787
0
    }
788
0
    if (underline) {
789
0
        (*m_stream) << "_";
790
0
        col += 1;
791
0
    }
792
0
    if (strikeOut) {
793
0
        (*m_stream) << "~~";
794
0
        col += 2;
795
0
    }
796
0
    if (missedBlankCodeBlockLine)
797
0
        (*m_stream) << qtmw_Newline;
798
0
    m_blockState.linePrefixWritten = false;
799
0
    return col;
800
0
}
801
802
QT_END_NAMESPACE