Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qtextmarkdownwriter.cpp
Go to the documentation of this file.
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
6#include "qfontinfo.h"
7#include "qfontmetrics.h"
9#include "qtextlist.h"
10#include "qtexttable.h"
11#include "qtextcursor.h"
15#include <QtCore/QRegularExpression>
16#if QT_CONFIG(itemmodel)
17#include "qabstractitemmodel.h"
18#endif
19
20#include <utility>
21
22QT_BEGIN_NAMESPACE
23
24using namespace Qt::StringLiterals;
25
26Q_STATIC_LOGGING_CATEGORY(lcMDW, "qt.text.markdown.writer")
27
28static const QChar qtmw_Space = u' ';
29static const QChar qtmw_Tab = u'\t';
30static const QChar qtmw_Newline = u'\n';
31static const QChar qtmw_CarriageReturn = u'\r';
32static const QChar qtmw_LineBreak = u'\x2028';
33static const QChar qtmw_DoubleQuote = u'"';
34static const QChar qtmw_Backtick = u'`';
35static const QChar qtmw_Backslash = u'\\';
36static const QChar qtmw_Period = u'.';
37
38QTextMarkdownWriter::QTextMarkdownWriter(QTextStream &stream, QTextDocument::MarkdownFeatures features)
39 : m_stream(&stream), m_features(features)
40{
41}
42
43bool QTextMarkdownWriter::writeAll(const QTextDocument *document)
44{
45 writeFrontMatter(document->metaInformation(QTextDocument::FrontMatter));
46 writeFrame(document->rootFrame());
47 return true;
48}
49
50#if QT_CONFIG(itemmodel)
51void QTextMarkdownWriter::writeTable(const QAbstractItemModel *table)
52{
53 QList<int> tableColumnWidths(table->columnCount());
54 for (int col = 0; col < table->columnCount(); ++col) {
55 tableColumnWidths[col] = table->headerData(col, Qt::Horizontal).toString().size();
56 for (int row = 0; row < table->rowCount(); ++row) {
57 tableColumnWidths[col] = qMax(tableColumnWidths[col],
58 table->data(table->index(row, col)).toString().size());
59 }
60 }
61
62 // write the header and separator
63 for (int col = 0; col < table->columnCount(); ++col) {
64 QString s = table->headerData(col, Qt::Horizontal).toString();
65 (*m_stream) << '|' << s << QString(tableColumnWidths[col] - s.size(), qtmw_Space);
66 }
67 (*m_stream) << "|" << Qt::endl;
68 for (int col = 0; col < tableColumnWidths.size(); ++col)
69 (*m_stream) << '|' << QString(tableColumnWidths[col], u'-');
70 (*m_stream) << '|'<< Qt::endl;
71
72 // write the body
73 for (int row = 0; row < table->rowCount(); ++row) {
74 for (int col = 0; col < table->columnCount(); ++col) {
75 QString s = table->data(table->index(row, col)).toString();
76 (*m_stream) << '|' << s << QString(tableColumnWidths[col] - s.size(), qtmw_Space);
77 }
78 (*m_stream) << '|'<< Qt::endl;
79 }
80 m_listInfo.clear();
81}
82#endif
83
84void QTextMarkdownWriter::writeFrontMatter(const QString &fm)
85{
86 const bool featureEnabled = m_features.testFlag(
87 static_cast<QTextDocument::MarkdownFeature>(QTextMarkdownImporter::FeatureFrontMatter));
88 qCDebug(lcMDW) << "writing FrontMatter?" << featureEnabled << "size" << fm.size();
89 if (fm.isEmpty() || !featureEnabled)
90 return;
91 (*m_stream) << "---\n"_L1 << fm;
92 if (!fm.endsWith(qtmw_Newline))
93 (*m_stream) << qtmw_Newline;
94 (*m_stream) << "---\n"_L1;
95}
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*/
108int QTextMarkdownWriter::measureCellWidth(const QTextTableCell &cell, bool ignoreFormat)
109{
110 QString scratch;
111 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 QScopedValueRollback stateRollback(m_blockState, BlockState{});
118 QScopedValueRollback streamRollback(m_stream, &scratchStream);
119
120 int width = 0;
121 for (auto it = cell.begin(); it != cell.end(); ++it) {
122 const QTextBlock block = it.currentBlock();
123 if (block.isValid())
124 width += writeBlock(block, false, ignoreFormat, true);
125 }
126
127 return width;
128}
129
130void QTextMarkdownWriter::writeFrame(const QTextFrame *frame)
131{
132 Q_ASSERT(frame);
133 const QTextTable *table = qobject_cast<const QTextTable*> (frame);
134 QTextFrame::iterator iterator = frame->begin();
135 QTextFrame *child = nullptr;
136 int tableRow = -1;
137 bool lastWasList = false;
138 QList<int> tableColumnWidths;
139 if (table) {
140 tableColumnWidths.resize(table->columns());
141 for (int col = 0; col < table->columns(); ++col) {
142 for (int row = 0; row < table->rows(); ++ row) {
143 QTextTableCell cell = table->cellAt(row, col);
144 if (cell.columnSpan() != 1)
145 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 const int cellTextLen = measureCellWidth(cell, row == 0);
151 if (tableColumnWidths[col] < cellTextLen)
152 tableColumnWidths[col] = cellTextLen;
153 }
154 }
155 }
156 while (!iterator.atEnd()) {
157 if (iterator.currentFrame() && child != iterator.currentFrame())
158 writeFrame(iterator.currentFrame());
159 else { // no frame, it's a block
160 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 bool nextIsDifferent = false;
164 bool ending = false;
165 int blockQuoteIndent = 0;
166 int nextBlockQuoteIndent = 0;
167 {
168 QTextFrame::iterator next = iterator;
169 ++next;
170 QTextBlockFormat format = iterator.currentBlock().blockFormat();
171 QTextBlockFormat nextFormat = next.currentBlock().blockFormat();
172 blockQuoteIndent = format.intProperty(QTextFormat::BlockQuoteLevel);
173 nextBlockQuoteIndent = nextFormat.intProperty(QTextFormat::BlockQuoteLevel);
174 if (next.atEnd()) {
175 nextIsDifferent = true;
176 ending = true;
177 } else {
178 if (nextFormat.indent() != format.indent() ||
179 nextFormat.property(QTextFormat::BlockCodeLanguage) !=
180 format.property(QTextFormat::BlockCodeLanguage))
181 nextIsDifferent = true;
182 }
183 }
184 if (table) {
185 QTextTableCell cell = table->cellAt(block.position());
186 if (tableRow < cell.row()) {
187 if (tableRow == 0) {
188 (*m_stream) << qtmw_Newline;
189 for (int col = 0; col < tableColumnWidths.size(); ++col)
190 (*m_stream) << '|' << QString(tableColumnWidths[col], u'-');
191 (*m_stream) << '|';
192 }
193 (*m_stream) << qtmw_Newline << '|';
194 tableRow = cell.row();
195 }
196 } else if (!block.textList()) {
197 if (lastWasList) {
198 (*m_stream) << qtmw_Newline;
199 m_blockState.linePrefixWritten = false;
200 }
201 }
202 int endingCol = writeBlock(block, !table, table && tableRow == 0,
203 nextIsDifferent && !block.textList());
204 m_blockState.doubleNewlineWritten = false;
205 if (table) {
206 QTextTableCell cell = table->cellAt(block.position());
207 int paddingLen = -endingCol;
208 int spanEndCol = cell.column() + cell.columnSpan();
209 for (int col = cell.column(); col < spanEndCol; ++col)
210 paddingLen += tableColumnWidths[col];
211 if (paddingLen > 0)
212 (*m_stream) << QString(paddingLen, qtmw_Space);
213 for (int col = cell.column(); col < spanEndCol; ++col)
214 (*m_stream) << "|";
215 } else if (m_blockState.fencedCodeBlock && ending) {
216 (*m_stream) << qtmw_Newline << m_blockState.linePrefix << QString(m_blockState.wrappedLineIndent, qtmw_Space)
217 << m_blockState.codeBlockFence << qtmw_Newline << qtmw_Newline;
218 m_blockState.codeBlockFence.clear();
219 } else if (m_blockState.indentedCodeBlock && nextIsDifferent) {
220 (*m_stream) << qtmw_Newline << qtmw_Newline;
221 } else if (endingCol > 0) {
222 if (block.textList() || block.blockFormat().hasProperty(QTextFormat::BlockCodeLanguage)) {
223 (*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 const bool quoteEndsWithItem = block.blockFormat().boolProperty(
227 QTextFormat::BlockQuoteInsideListItem);
228 if (block.textList() && !quoteEndsWithItem) {
229 (*m_stream) << m_blockState.linePrefix;
230 m_blockState.linePrefixWritten = true;
231 }
232 } else {
233 (*m_stream) << qtmw_Newline;
234 if (nextBlockQuoteIndent < blockQuoteIndent)
235 setLinePrefixForBlockQuote(nextBlockQuoteIndent);
236 (*m_stream) << m_blockState.linePrefix;
237 (*m_stream) << qtmw_Newline;
238 m_blockState.doubleNewlineWritten = true;
239 }
240 }
241 lastWasList = block.textList();
242 }
243 child = iterator.currentFrame();
244 ++iterator;
245 }
246 if (table) {
247 (*m_stream) << qtmw_Newline << qtmw_Newline;
248 m_blockState.doubleNewlineWritten = true;
249 }
250 m_listInfo.clear();
251}
252
253QTextMarkdownWriter::ListInfo QTextMarkdownWriter::listInfo(QTextList *list)
254{
255 if (!m_listInfo.contains(list)) {
256 // decide whether this list is loose or tight
257 ListInfo info;
258 info.loose = false;
259 if (list->count() > 1) {
260 QTextBlock first = list->item(0);
261 QTextBlock last = list->item(list->count() - 1);
262 QTextBlock next = first.next();
263 while (next.isValid()) {
264 if (next == last)
265 break;
266 qCDebug(lcMDW) << "next block in list" << list << next.text() << "part of list?" << next.textList();
267 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 qCDebug(lcMDW) << "decided list beginning with" << first.text() << "is loose after" << next.text();
271 info.loose = true;
272 break;
273 }
274 next = next.next();
275 }
276 }
277 m_listInfo.insert(list, info);
278 return info;
279 }
280 return m_listInfo.value(list);
281}
282
283void QTextMarkdownWriter::setLinePrefixForBlockQuote(int level)
284{
285 m_blockState.linePrefix.clear();
286 if (level > 0) {
287 m_blockState.linePrefix.reserve(level * 2);
288 for (int i = 0; i < level; ++i)
289 m_blockState.linePrefix += u"> ";
290 }
291}
292
293static int nearestWordWrapIndex(const QString &s, int before)
294{
295 before = qMin(before, s.size());
296 int fragBegin = qMax(before - 15, 0);
297 if (lcMDW().isDebugEnabled()) {
298 QString frag = s.mid(fragBegin, 30);
299 qCDebug(lcMDW) << frag << before;
300 qCDebug(lcMDW) << QString(before - fragBegin, qtmw_Period) + u'<';
301 }
302 for (int i = before - 1; i >= 0; --i) {
303 if (s.at(i).isSpace()) {
304 qCDebug(lcMDW) << QString(i - fragBegin, qtmw_Period) + u'^' << i;
305 return i;
306 }
307 }
308 qCDebug(lcMDW, "not possible");
309 return -1;
310}
311
312static int adjacentBackticksCount(const QString &s)
313{
314 int start = -1, len = s.size();
315 int ret = 0;
316 for (int i = 0; i < len; ++i) {
317 if (s.at(i) == qtmw_Backtick) {
318 if (start < 0)
319 start = i;
320 } else if (start >= 0) {
321 ret = qMax(ret, i - start);
322 start = -1;
323 }
324 }
325 if (s.at(len - 1) == qtmw_Backtick)
326 ret = qMax(ret, len - start);
327 return ret;
328}
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*/
336static void maybeEscapeFirstChar(QString &s)
337{
338 static const QRegularExpression numericListRe(uR"(\d+([\.)])\s)"_s);
339 constexpr auto specialFirstCharacters = "#*+-"_L1;
340
341 QString sTrimmed = s.trimmed();
342 if (sTrimmed.isEmpty())
343 return;
344 QChar firstChar = sTrimmed.at(0);
345 if (specialFirstCharacters.contains(firstChar)) {
346 int i = s.indexOf(firstChar); // == 0 unless s got trimmed
347 s.insert(i, u'\\');
348 } else {
349 auto match = numericListRe.match(s, 0, QRegularExpression::NormalMatch,
350 QRegularExpression::AnchorAtOffsetMatchOption);
351 if (match.hasMatch())
352 s.insert(match.capturedStart(1), qtmw_Backslash);
353 }
354}
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*/
361static void escapeSpecialCharacters(QString &s)
362{
363 static const QRegularExpression spaceRe(uR"(\s+)"_s);
364 static const QRegularExpression specialRe(uR"([<!*[`&]+[/\w])"_s);
365
366 s.replace("\\"_L1, "\\\\"_L1);
367
368 int i = 0;
369 while (i >= 0) {
370 if (int j = s.indexOf(specialRe, i); j >= 0) {
371 s.insert(j, qtmw_Backslash);
372 i = j + 3;
373 }
374 i = s.indexOf(spaceRe, i);
375 if (i >= 0)
376 ++i; // past the whitespace, if found
377 }
378}
379
384
385static LineEndPositions findLineEnd(const QChar *begin, const QChar *end)
386{
387 LineEndPositions result{ end, end };
388
389 while (begin < end) {
390 if (*begin == qtmw_Newline) {
391 result.lineEnd = begin;
392 result.nextLineBegin = begin + 1;
393 break;
394 } else if (*begin == qtmw_CarriageReturn) {
395 result.lineEnd = begin;
396 result.nextLineBegin = begin + 1;
397 if (((begin + 1) < end) && begin[1] == qtmw_Newline)
398 ++result.nextLineBegin;
399 break;
400 }
401
402 ++begin;
403 }
404
405 return result;
406}
407
408static bool isBlankLine(const QChar *begin, const QChar *end)
409{
410 while (begin < end) {
411 if (*begin != qtmw_Space && *begin != qtmw_Tab)
412 return false;
413 ++begin;
414 }
415 return true;
416}
417
418static QString createLinkTitle(const QString &title)
419{
420 QString result;
421 result.reserve(title.size() + 2);
422 result += qtmw_DoubleQuote;
423
424 const QChar *data = title.data();
425 const QChar *end = data + title.size();
426
427 while (data < end) {
428 const auto lineEndPositions = findLineEnd(data, end);
429
430 if (!isBlankLine(data, lineEndPositions.lineEnd)) {
431 while (data < lineEndPositions.nextLineBegin) {
432 if (*data == qtmw_DoubleQuote)
433 result += qtmw_Backslash;
434 result += *data;
435 ++data;
436 }
437 }
438
439 data = lineEndPositions.nextLineBegin;
440 }
441
442 result += qtmw_DoubleQuote;
443 return result;
444}
445
446int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ignoreFormat, bool ignoreEmpty)
447{
448 if (block.text().isEmpty() && ignoreEmpty)
449 return 0;
450 const int ColumnLimit = 80;
451 QTextBlockFormat blockFmt = block.blockFormat();
452 bool missedBlankCodeBlockLine = false;
453 const bool codeBlock = blockFmt.hasProperty(QTextFormat::BlockCodeFence) ||
454 blockFmt.stringProperty(QTextFormat::BlockCodeLanguage).size() > 0 ||
455 blockFmt.nonBreakableLines();
456 const int blockQuoteLevel = blockFmt.intProperty(QTextFormat::BlockQuoteLevel);
457 if (m_blockState.fencedCodeBlock && !codeBlock) {
458 (*m_stream) << m_blockState.linePrefix << m_blockState.codeBlockFence << qtmw_Newline;
459 m_blockState.fencedCodeBlock = false;
460 m_blockState.codeBlockFence.clear();
461 m_blockState.linePrefixWritten = m_blockState.linePrefix.size() > 0;
462 }
463 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 const bool quoteInListItem = blockQuoteLevel > 0 && block.textList()
467 && blockFmt.boolProperty(QTextFormat::BlockQuoteInsideListItem);
468 if (!blockFmt.headingLevel() && blockQuoteLevel > 0) {
469 setLinePrefixForBlockQuote(blockQuoteLevel);
470 if (!m_blockState.linePrefixWritten && !quoteInListItem) {
471 (*m_stream) << m_blockState.linePrefix;
472 m_blockState.linePrefixWritten = true;
473 }
474 }
475 if (block.textList()) { // it's a list-item
476 auto fmt = block.textList()->format();
477 const int listLevel = fmt.indent();
478 // Negative numbers don't start a list in Markdown, so ignore them.
479 const int start = fmt.start() >= 0 ? fmt.start() : 1;
480 const int number = block.textList()->itemNumber(block) + start;
481 QByteArray bullet = " ";
482 bool numeric = false;
483 switch (fmt.style()) {
484 case QTextListFormat::ListDisc:
485 bullet = "-";
486 m_blockState.wrappedLineIndent = 2;
487 break;
488 case QTextListFormat::ListCircle:
489 bullet = "*";
490 m_blockState.wrappedLineIndent = 2;
491 break;
492 case QTextListFormat::ListSquare:
493 bullet = "+";
494 m_blockState.wrappedLineIndent = 2;
495 break;
496 case QTextListFormat::ListStyleUndefined: break;
497 case QTextListFormat::ListDecimal:
498 case QTextListFormat::ListLowerAlpha:
499 case QTextListFormat::ListUpperAlpha:
500 case QTextListFormat::ListLowerRoman:
501 case QTextListFormat::ListUpperRoman:
502 numeric = true;
503 m_blockState.wrappedLineIndent = 4;
504 break;
505 }
506 switch (blockFmt.marker()) {
507 case QTextBlockFormat::MarkerType::Checked:
508 bullet += " [x]";
509 break;
510 case QTextBlockFormat::MarkerType::Unchecked:
511 bullet += " [ ]";
512 break;
513 default:
514 break;
515 }
516 int indentFirstLine = (listLevel - 1) * (numeric ? 4 : 2);
517 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 const bool cannotInterruptParagraph =
525 numeric && number != 1 && listLevel > m_blockState.lastListIndent;
526 if (m_blockState.lastListIndent != listLevel && !m_blockState.doubleNewlineWritten
527 && (cannotInterruptParagraph || listInfo(block.textList()).loose))
528 (*m_stream) << qtmw_Newline;
529 m_blockState.lastListIndent = listLevel;
530 QString prefix(indentFirstLine, qtmw_Space);
531 if (numeric) {
532 QString suffix = fmt.numberSuffix();
533 if (suffix.isEmpty())
534 suffix = QString(qtmw_Period);
535 QString numberStr = QString::number(number) + suffix + qtmw_Space;
536 if (numberStr.size() == 3)
537 numberStr += qtmw_Space;
538 prefix += numberStr;
539 } else {
540 prefix += QLatin1StringView(bullet) + qtmw_Space;
541 }
542 if (quoteInListItem) {
543 // "- > ": quote nested inside list item
544 prefix += m_blockState.linePrefix;
545 m_blockState.linePrefixWritten = true;
546 }
547 (*m_stream) << prefix;
548 } else if (blockFmt.hasProperty(QTextFormat::BlockTrailingHorizontalRulerWidth)) {
549 (*m_stream) << "- - -\n"; // unambiguous horizontal rule, not an underline under a heading
550 return 0;
551 } 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 if (!blockFmt.hasProperty(QTextFormat::BlockQuoteLevel))
555 missedBlankCodeBlockLine = true; // only if we don't get any fragments below
556 if (!m_blockState.fencedCodeBlock) {
557 QString fenceChar = blockFmt.stringProperty(QTextFormat::BlockCodeFence);
558 if (fenceChar.isEmpty())
559 fenceChar = "`"_L1;
560 m_blockState.codeBlockFence = QString(3, fenceChar.at(0));
561 if (blockFmt.hasProperty(QTextFormat::BlockIndent))
562 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 (*m_stream) << m_blockState.codeBlockFence << blockFmt.stringProperty(QTextFormat::BlockCodeLanguage)
565 << qtmw_Newline << m_blockState.linePrefix;
566 m_blockState.fencedCodeBlock = true;
567 }
568 wrap = false;
569 } else if (!blockFmt.indent()) {
570 m_blockState.wrappedLineIndent = 0;
571 if (blockFmt.hasProperty(QTextFormat::BlockCodeLanguage)) {
572 // A block quote can contain an indented code block, but not vice-versa.
573 m_blockState.linePrefix += QString(4, qtmw_Space);
574 m_blockState.indentedCodeBlock = true;
575 }
576 if (!m_blockState.linePrefixWritten) {
577 (*m_stream) << m_blockState.linePrefix;
578 m_blockState.linePrefixWritten = true;
579 }
580 }
581 if (blockFmt.headingLevel()) {
582 (*m_stream) << QByteArray(blockFmt.headingLevel(), '#') << ' ';
583 wrap = false;
584 }
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 const QString wrappedLineIndentString(m_blockState.wrappedLineIndent, qtmw_Space);
589 QString wrapIndentString;
590 if (quoteInListItem)
591 wrapIndentString = wrappedLineIndentString + m_blockState.linePrefix;
592 else
593 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 int col = wrapIndentString.size();
598 bool mono = false;
599 bool startsOrEndsWithBacktick = false;
600 bool bold = false;
601 bool italic = false;
602 bool underline = false;
603 bool strikeOut = false;
604 bool endingMarkers = false;
605 QString backticks(qtmw_Backtick);
606 for (QTextBlock::Iterator frag = block.begin(); !frag.atEnd(); ++frag) {
607 missedBlankCodeBlockLine = false;
608 QString fragmentText = frag.fragment().text();
609 while (fragmentText.endsWith(qtmw_Newline))
610 fragmentText.chop(1);
611 if (!(m_blockState.fencedCodeBlock || m_blockState.indentedCodeBlock)) {
612 escapeSpecialCharacters(fragmentText);
613 maybeEscapeFirstChar(fragmentText);
614 }
615 if (block.textList()) { // <li>first line</br>continuation</li>
616 QString newlineIndent =
617 QString(qtmw_Newline) + QString(m_blockState.wrappedLineIndent, qtmw_Space);
618 fragmentText.replace(QString(qtmw_LineBreak), newlineIndent);
619 } else if (blockFmt.indent() > 0) { // <li>first line<p>continuation</p></li>
620 (*m_stream) << QString(m_blockState.wrappedLineIndent, qtmw_Space);
621 } else {
622 fragmentText.replace(qtmw_LineBreak, qtmw_Newline);
623 }
624 startsOrEndsWithBacktick |=
625 fragmentText.startsWith(qtmw_Backtick) || fragmentText.endsWith(qtmw_Backtick);
626 QTextCharFormat fmt = frag.fragment().charFormat();
627 if (fmt.isImageFormat()) {
628 QTextImageFormat ifmt = fmt.toImageFormat();
629 QString desc = ifmt.stringProperty(QTextFormat::ImageAltText);
630 if (desc.isEmpty())
631 desc = "image"_L1;
632 QString s = "!["_L1 + desc + "]("_L1 + ifmt.name();
633 QString title = ifmt.stringProperty(QTextFormat::ImageTitle);
634 if (!title.isEmpty())
635 s += qtmw_Space + qtmw_DoubleQuote + title + qtmw_DoubleQuote;
636 s += u')';
637 if (wrap && col + s.size() > ColumnLimit) {
638 (*m_stream) << qtmw_Newline << wrapIndentString;
639 col = m_blockState.wrappedLineIndent;
640 }
641 (*m_stream) << s;
642 col += s.size();
643 } else if (fmt.hasProperty(QTextFormat::AnchorHref)) {
644 const auto href = fmt.property(QTextFormat::AnchorHref).toString();
645 const bool hasToolTip = fmt.hasProperty(QTextFormat::TextToolTip);
646 QString s;
647 if (!hasToolTip && href == fragmentText && !QUrl(href, QUrl::StrictMode).scheme().isEmpty()) {
648 s = u'<' + href + u'>';
649 } else {
650 s = u'[' + fragmentText + "]("_L1 + href;
651 if (hasToolTip) {
652 s += qtmw_Space;
653 s += createLinkTitle(fmt.property(QTextFormat::TextToolTip).toString());
654 }
655 s += u')';
656 }
657 if (wrap && col + s.size() > ColumnLimit) {
658 (*m_stream) << qtmw_Newline << wrapIndentString;
659 col = m_blockState.wrappedLineIndent;
660 }
661 (*m_stream) << s;
662 col += s.size();
663 } else {
664 QFontInfo fontInfo(fmt.font());
665 bool monoFrag = fontInfo.fixedPitch() || fmt.fontFixedPitch();
666 QString markers;
667 if (!ignoreFormat) {
668 if (monoFrag != mono && !m_blockState.indentedCodeBlock && !m_blockState.fencedCodeBlock) {
669 if (monoFrag)
670 backticks =
671 QString(adjacentBackticksCount(fragmentText) + 1, qtmw_Backtick);
672 markers += backticks;
673 if (startsOrEndsWithBacktick)
674 markers += qtmw_Space;
675 mono = monoFrag;
676 if (!mono)
677 endingMarkers = true;
678 }
679 if (!blockFmt.headingLevel() && !mono) {
680 if (fontInfo.bold() != bold) {
681 markers += "**"_L1;
682 bold = fontInfo.bold();
683 if (!bold)
684 endingMarkers = true;
685 }
686 if (fontInfo.italic() != italic) {
687 markers += u'*';
688 italic = fontInfo.italic();
689 if (!italic)
690 endingMarkers = true;
691 }
692 if (fontInfo.strikeOut() != strikeOut) {
693 markers += "~~"_L1;
694 strikeOut = fontInfo.strikeOut();
695 if (!strikeOut)
696 endingMarkers = true;
697 }
698 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 markers += u'_';
703 underline = fontInfo.underline();
704 if (!underline)
705 endingMarkers = true;
706 }
707 }
708 }
709 if (wrap && col + markers.size() * 2 + fragmentText.size() > ColumnLimit) {
710 int i = 0;
711 const int fragLen = fragmentText.size();
712 bool breakingLine = false;
713 while (i < fragLen) {
714 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 if (endingMarkers) {
718 (*m_stream) << markers;
719 markers.clear();
720 }
721 (*m_stream) << qtmw_Newline << wrapIndentString;
722 col = m_blockState.wrappedLineIndent;
723 while (i < fragLen && fragmentText[i].isSpace())
724 ++i;
725 }
726 int j = i + ColumnLimit - col;
727 if (j < fragLen) {
728 int wi = nearestWordWrapIndex(fragmentText, j);
729 if (wi < 0) {
730 j = fragLen;
731 // can't break within the fragment: we need to break already _before_ it
732 if (endingMarkers) {
733 (*m_stream) << markers;
734 markers.clear();
735 }
736 (*m_stream) << qtmw_Newline << wrapIndentString;
737 col = m_blockState.wrappedLineIndent;
738 } else if (wi >= i) {
739 j = wi;
740 breakingLine = true;
741 }
742 } else {
743 j = fragLen;
744 breakingLine = false;
745 }
746 QString subfrag = fragmentText.mid(i, j - i);
747 if (!i) {
748 (*m_stream) << markers;
749 col += markers.size();
750 }
751 if (col == m_blockState.wrappedLineIndent)
752 maybeEscapeFirstChar(subfrag);
753 (*m_stream) << subfrag;
754 if (breakingLine) {
755 (*m_stream) << qtmw_Newline << wrapIndentString;
756 col = m_blockState.wrappedLineIndent;
757 } else {
758 col += subfrag.size();
759 }
760 i = j + 1;
761 } // loop over fragment characters (we know we need to break somewhere)
762 } else {
763 if (!m_blockState.linePrefixWritten && col == wrapIndentString.size()) {
764 (*m_stream) << m_blockState.linePrefix;
765 col += m_blockState.linePrefix.size();
766 }
767 (*m_stream) << markers << fragmentText;
768 col += markers.size() + fragmentText.size();
769 }
770 }
771 }
772 if (mono) {
773 if (startsOrEndsWithBacktick) {
774 (*m_stream) << qtmw_Space;
775 col += 1;
776 }
777 (*m_stream) << backticks;
778 col += backticks.size();
779 }
780 if (bold) {
781 (*m_stream) << "**";
782 col += 2;
783 }
784 if (italic) {
785 (*m_stream) << "*";
786 col += 1;
787 }
788 if (underline) {
789 (*m_stream) << "_";
790 col += 1;
791 }
792 if (strikeOut) {
793 (*m_stream) << "~~";
794 col += 2;
795 }
796 if (missedBlankCodeBlockLine)
797 (*m_stream) << qtmw_Newline;
798 m_blockState.linePrefixWritten = false;
799 return col;
800}
801
802QT_END_NAMESPACE
#define qCDebug(category,...)
#define Q_STATIC_LOGGING_CATEGORY(name,...)
static const QChar qtmw_Period
static const QChar qtmw_LineBreak
static const QChar qtmw_Space
static const QChar qtmw_DoubleQuote
static int nearestWordWrapIndex(const QString &s, int before)
static const QChar qtmw_Backtick
static const QChar qtmw_CarriageReturn
static int adjacentBackticksCount(const QString &s)
static QString createLinkTitle(const QString &title)
static bool isBlankLine(const QChar *begin, const QChar *end)
static const QChar qtmw_Backslash
static void maybeEscapeFirstChar(QString &s)
static const QChar qtmw_Tab
static const QChar qtmw_Newline
static LineEndPositions findLineEnd(const QChar *begin, const QChar *end)
static void escapeSpecialCharacters(QString &s)