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

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