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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

source code of qtbase/src/gui/text/qtextmarkdownwriter.cpp