Coverage Report

Created: 2026-02-10 07:39

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