1/****************************************************************************
2**
3** Copyright (C) 2019 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the test suite of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include <QtTest/QtTest>
30#include <QTextDocument>
31#include <QTextCursor>
32#include <QTextBlock>
33#include <QTextList>
34#include <QTextTable>
35#include <QBuffer>
36#include <QDebug>
37
38#include <private/qtextmarkdownwriter_p.h>
39
40// #define DEBUG_WRITE_OUTPUT
41
42class tst_QTextMarkdownWriter : public QObject
43{
44 Q_OBJECT
45public slots:
46 void init();
47 void cleanup();
48
49private slots:
50 void testWriteParagraph_data();
51 void testWriteParagraph();
52 void testWriteList();
53 void testWriteEmptyList();
54 void testWriteCheckboxListItemEndingWithCode();
55 void testWriteNestedBulletLists_data();
56 void testWriteNestedBulletLists();
57 void testWriteNestedNumericLists();
58 void testWriteTable();
59 void rewriteDocument_data();
60 void rewriteDocument();
61 void fromHtml_data();
62 void fromHtml();
63
64private:
65 QString documentToUnixMarkdown();
66
67private:
68 QTextDocument *document;
69};
70
71void tst_QTextMarkdownWriter::init()
72{
73 document = new QTextDocument();
74}
75
76void tst_QTextMarkdownWriter::cleanup()
77{
78 delete document;
79}
80
81void tst_QTextMarkdownWriter::testWriteParagraph_data()
82{
83 QTest::addColumn<QString>(name: "input");
84 QTest::addColumn<QString>(name: "output");
85
86 QTest::newRow(dataTag: "empty") << "" <<
87 "";
88 QTest::newRow(dataTag: "spaces") << "foobar word" <<
89 "foobar word\n\n";
90 QTest::newRow(dataTag: "starting spaces") << " starting spaces" <<
91 " starting spaces\n\n";
92 QTest::newRow(dataTag: "trailing spaces") << "trailing spaces " <<
93 "trailing spaces \n\n";
94 QTest::newRow(dataTag: "tab") << "word\ttab x" <<
95 "word\ttab x\n\n";
96 QTest::newRow(dataTag: "tab2") << "word\t\ttab\tx" <<
97 "word\t\ttab\tx\n\n";
98 QTest::newRow(dataTag: "misc") << "foobar word\ttab x" <<
99 "foobar word\ttab x\n\n";
100 QTest::newRow(dataTag: "misc2") << "\t \tFoo" <<
101 "\t \tFoo\n\n";
102}
103
104void tst_QTextMarkdownWriter::testWriteParagraph()
105{
106 QFETCH(QString, input);
107 QFETCH(QString, output);
108
109 QTextCursor cursor(document);
110 cursor.insertText(text: input);
111
112 QCOMPARE(documentToUnixMarkdown(), output);
113}
114
115void tst_QTextMarkdownWriter::testWriteList()
116{
117 QTextCursor cursor(document);
118 QTextList *list = cursor.createList(style: QTextListFormat::ListDisc);
119 cursor.insertText(text: "ListItem 1");
120 list->add(block: cursor.block());
121 cursor.insertBlock();
122 cursor.insertText(text: "ListItem 2");
123 list->add(block: cursor.block());
124
125 QCOMPARE(documentToUnixMarkdown(), QString::fromLatin1(
126 "- ListItem 1\n- ListItem 2\n"));
127}
128
129void tst_QTextMarkdownWriter::testWriteEmptyList()
130{
131 QTextCursor cursor(document);
132 cursor.createList(style: QTextListFormat::ListDisc);
133
134 QCOMPARE(documentToUnixMarkdown(), QString::fromLatin1("- \n"));
135}
136
137void tst_QTextMarkdownWriter::testWriteCheckboxListItemEndingWithCode()
138{
139 QTextCursor cursor(document);
140 QTextList *list = cursor.createList(style: QTextListFormat::ListDisc);
141 cursor.insertText(text: "Image.originalSize property (not necessary; PdfDocument.pagePointSize() substitutes)");
142 list->add(block: cursor.block());
143 {
144 auto fmt = cursor.block().blockFormat();
145 fmt.setMarker(QTextBlockFormat::MarkerType::Unchecked);
146 cursor.setBlockFormat(fmt);
147 }
148 cursor.movePosition(op: QTextCursor::PreviousWord, QTextCursor::MoveAnchor, n: 2);
149 cursor.movePosition(op: QTextCursor::Left, QTextCursor::MoveAnchor);
150 cursor.movePosition(op: QTextCursor::PreviousWord, QTextCursor::KeepAnchor, n: 4);
151 QCOMPARE(cursor.selectedText(), QString::fromLatin1("PdfDocument.pagePointSize()"));
152 auto fmt = cursor.charFormat();
153 fmt.setFontFixedPitch(true);
154 cursor.setCharFormat(fmt);
155 cursor.movePosition(op: QTextCursor::PreviousWord, QTextCursor::MoveAnchor, n: 5);
156 cursor.movePosition(op: QTextCursor::Left, QTextCursor::MoveAnchor);
157 cursor.movePosition(op: QTextCursor::PreviousWord, QTextCursor::KeepAnchor, n: 4);
158 QCOMPARE(cursor.selectedText(), QString::fromLatin1("Image.originalSize"));
159 cursor.setCharFormat(fmt);
160
161 QCOMPARE(documentToUnixMarkdown(), QString::fromLatin1(
162 "- [ ] `Image.originalSize` property (not necessary; `PdfDocument.pagePointSize()`\n substitutes)\n"));
163}
164
165void tst_QTextMarkdownWriter::testWriteNestedBulletLists_data()
166{
167 QTest::addColumn<bool>(name: "checkbox");
168 QTest::addColumn<bool>(name: "checked");
169 QTest::addColumn<bool>(name: "continuationLine");
170 QTest::addColumn<bool>(name: "continuationParagraph");
171 QTest::addColumn<QString>(name: "expectedOutput");
172
173 QTest::newRow(dataTag: "plain bullets") << false << false << false << false <<
174 "- ListItem 1\n * ListItem 2\n + ListItem 3\n- ListItem 4\n * ListItem 5\n";
175 QTest::newRow(dataTag: "bullets with continuation lines") << false << false << true << false <<
176 "- ListItem 1\n * ListItem 2\n + ListItem 3 with text that won't fit on one line and thus needs a\n continuation\n- ListItem 4\n * ListItem 5 with text that won't fit on one line and thus needs a\n continuation\n";
177 QTest::newRow(dataTag: "bullets with continuation paragraphs") << false << false << false << true <<
178 "- ListItem 1\n\n * ListItem 2\n + ListItem 3\n\n continuation\n\n- ListItem 4\n\n * ListItem 5\n\n continuation\n\n";
179 QTest::newRow(dataTag: "unchecked") << true << false << false << false <<
180 "- [ ] ListItem 1\n * [ ] ListItem 2\n + [ ] ListItem 3\n- [ ] ListItem 4\n * [ ] ListItem 5\n";
181 QTest::newRow(dataTag: "checked") << true << true << false << false <<
182 "- [x] ListItem 1\n * [x] ListItem 2\n + [x] ListItem 3\n- [x] ListItem 4\n * [x] ListItem 5\n";
183 QTest::newRow(dataTag: "checked with continuation lines") << true << true << true << false <<
184 "- [x] ListItem 1\n * [x] ListItem 2\n + [x] ListItem 3 with text that won't fit on one line and thus needs a\n continuation\n- [x] ListItem 4\n * [x] ListItem 5 with text that won't fit on one line and thus needs a\n continuation\n";
185 QTest::newRow(dataTag: "checked with continuation paragraphs") << true << true << false << true <<
186 "- [x] ListItem 1\n\n * [x] ListItem 2\n + [x] ListItem 3\n\n continuation\n\n- [x] ListItem 4\n\n * [x] ListItem 5\n\n continuation\n\n";
187}
188
189void tst_QTextMarkdownWriter::testWriteNestedBulletLists()
190{
191 QFETCH(bool, checkbox);
192 QFETCH(bool, checked);
193 QFETCH(bool, continuationParagraph);
194 QFETCH(bool, continuationLine);
195 QFETCH(QString, expectedOutput);
196
197 QTextCursor cursor(document);
198 QTextBlockFormat blockFmt = cursor.blockFormat();
199 if (checkbox) {
200 blockFmt.setMarker(checked ? QTextBlockFormat::MarkerType::Checked : QTextBlockFormat::MarkerType::Unchecked);
201 cursor.setBlockFormat(blockFmt);
202 }
203
204 QTextList *list1 = cursor.createList(style: QTextListFormat::ListDisc);
205 cursor.insertText(text: "ListItem 1");
206 list1->add(block: cursor.block());
207
208 QTextListFormat fmt2;
209 fmt2.setStyle(QTextListFormat::ListCircle);
210 fmt2.setIndent(2);
211 QTextList *list2 = cursor.insertList(format: fmt2);
212 cursor.insertText(text: "ListItem 2");
213
214 QTextListFormat fmt3;
215 fmt3.setStyle(QTextListFormat::ListSquare);
216 fmt3.setIndent(3);
217 cursor.insertList(format: fmt3);
218 cursor.insertText(text: continuationLine ?
219 "ListItem 3 with text that won't fit on one line and thus needs a continuation" :
220 "ListItem 3");
221 if (continuationParagraph) {
222 QTextBlockFormat blockFmt;
223 blockFmt.setIndent(2);
224 cursor.insertBlock(format: blockFmt);
225 cursor.insertText(text: "continuation");
226 }
227
228 cursor.insertBlock(format: blockFmt);
229 cursor.insertText(text: "ListItem 4");
230 list1->add(block: cursor.block());
231
232 cursor.insertBlock();
233 cursor.insertText(text: continuationLine ?
234 "ListItem 5 with text that won't fit on one line and thus needs a continuation" :
235 "ListItem 5");
236 list2->add(block: cursor.block());
237 if (continuationParagraph) {
238 QTextBlockFormat blockFmt;
239 blockFmt.setIndent(2);
240 cursor.insertBlock(format: blockFmt);
241 cursor.insertText(text: "continuation");
242 }
243
244 QString output = documentToUnixMarkdown();
245#ifdef DEBUG_WRITE_OUTPUT
246 {
247 QFile out("/tmp/" + QLatin1String(QTest::currentDataTag()) + ".md");
248 out.open(QFile::WriteOnly);
249 out.write(output.toUtf8());
250 out.close();
251 }
252#endif
253 QCOMPARE(documentToUnixMarkdown(), expectedOutput);
254}
255
256void tst_QTextMarkdownWriter::testWriteNestedNumericLists()
257{
258 QTextCursor cursor(document);
259
260 QTextList *list1 = cursor.createList(style: QTextListFormat::ListDecimal);
261 cursor.insertText(text: "ListItem 1");
262 list1->add(block: cursor.block());
263
264 QTextListFormat fmt2;
265 fmt2.setStyle(QTextListFormat::ListLowerAlpha);
266 fmt2.setNumberSuffix(QLatin1String(")"));
267 fmt2.setIndent(2);
268 QTextList *list2 = cursor.insertList(format: fmt2);
269 cursor.insertText(text: "ListItem 2");
270
271 QTextListFormat fmt3;
272 fmt3.setStyle(QTextListFormat::ListDecimal);
273 fmt3.setIndent(3);
274 cursor.insertList(format: fmt3);
275 cursor.insertText(text: "ListItem 3");
276
277 cursor.insertBlock();
278 cursor.insertText(text: "ListItem 4");
279 list1->add(block: cursor.block());
280
281 cursor.insertBlock();
282 cursor.insertText(text: "ListItem 5");
283 list2->add(block: cursor.block());
284
285 // There's no QTextList API to set the starting number so we hard-coded all lists to start at 1 (QTBUG-65384)
286 QCOMPARE(documentToUnixMarkdown(), QString::fromLatin1(
287 "1. ListItem 1\n 1) ListItem 2\n 1. ListItem 3\n2. ListItem 4\n 2) ListItem 5\n"));
288}
289
290void tst_QTextMarkdownWriter::testWriteTable()
291{
292 QTextCursor cursor(document);
293 QTextTable * table = cursor.insertTable(rows: 4, cols: 3);
294 cursor = table->cellAt(row: 0, col: 0).firstCursorPosition();
295 // valid Markdown tables need headers, but QTextTable doesn't make that distinction
296 // so QTextMarkdownWriter assumes the first row of any table is a header
297 cursor.insertText(text: "one");
298 cursor.movePosition(op: QTextCursor::NextCell);
299 cursor.insertText(text: "two");
300 cursor.movePosition(op: QTextCursor::NextCell);
301 cursor.insertText(text: "three");
302 cursor.movePosition(op: QTextCursor::NextCell);
303
304 cursor.insertText(text: "alice");
305 cursor.movePosition(op: QTextCursor::NextCell);
306 cursor.insertText(text: "bob");
307 cursor.movePosition(op: QTextCursor::NextCell);
308 cursor.insertText(text: "carl");
309 cursor.movePosition(op: QTextCursor::NextCell);
310
311 cursor.insertText(text: "dennis");
312 cursor.movePosition(op: QTextCursor::NextCell);
313 cursor.insertText(text: "eric");
314 cursor.movePosition(op: QTextCursor::NextCell);
315 cursor.insertText(text: "fiona");
316 cursor.movePosition(op: QTextCursor::NextCell);
317
318 cursor.insertText(text: "gina");
319 /*
320 |one |two |three|
321 |------|----|-----|
322 |alice |bob |carl |
323 |dennis|eric|fiona|
324 |gina | | |
325 */
326
327 QString md = documentToUnixMarkdown();
328
329#ifdef DEBUG_WRITE_OUTPUT
330 {
331 QFile out("/tmp/table.md");
332 out.open(QFile::WriteOnly);
333 out.write(md.toUtf8());
334 out.close();
335 }
336#endif
337
338 QString expected = QString::fromLatin1(
339 str: "\n|one |two |three|\n|------|----|-----|\n|alice |bob |carl |\n|dennis|eric|fiona|\n|gina | | |\n\n");
340 QCOMPARE(md, expected);
341
342 // create table with merged cells
343 document->clear();
344 cursor = QTextCursor(document);
345 table = cursor.insertTable(rows: 3, cols: 3);
346 table->mergeCells(row: 0, col: 0, numRows: 1, numCols: 2);
347 table->mergeCells(row: 1, col: 1, numRows: 1, numCols: 2);
348 cursor = table->cellAt(row: 0, col: 0).firstCursorPosition();
349 cursor.insertText(text: "a");
350 cursor.movePosition(op: QTextCursor::NextCell);
351 cursor.insertText(text: "b");
352 cursor.movePosition(op: QTextCursor::NextCell);
353 cursor.insertText(text: "c");
354 cursor.movePosition(op: QTextCursor::NextCell);
355 cursor.insertText(text: "d");
356 cursor.movePosition(op: QTextCursor::NextCell);
357 cursor.insertText(text: "e");
358 cursor.movePosition(op: QTextCursor::NextCell);
359 cursor.insertText(text: "f");
360 /*
361 +---+-+
362 |a |b|
363 +---+-+
364 |c| d|
365 +-+-+-+
366 |e|f| |
367 +-+-+-+
368
369 generates
370
371 |a ||b|
372 |-|-|-|
373 |c|d ||
374 |e|f| |
375
376 */
377
378 md = documentToUnixMarkdown();
379
380#ifdef DEBUG_WRITE_OUTPUT
381 {
382 QFile out("/tmp/table-merged-cells.md");
383 out.open(QFile::WriteOnly);
384 out.write(md.toUtf8());
385 out.close();
386 }
387#endif
388
389 QCOMPARE(md, QString::fromLatin1("\n|a ||b|\n|-|-|-|\n|c|d ||\n|e|f| |\n\n"));
390}
391
392void tst_QTextMarkdownWriter::rewriteDocument_data()
393{
394 QTest::addColumn<QString>(name: "inputFile");
395
396 QTest::newRow(dataTag: "block quotes") << "blockquotes.md";
397 QTest::newRow(dataTag: "example") << "example.md";
398 QTest::newRow(dataTag: "list items after headings") << "headingsAndLists.md";
399 QTest::newRow(dataTag: "word wrap") << "wordWrap.md";
400 QTest::newRow(dataTag: "links") << "links.md";
401 QTest::newRow(dataTag: "lists and code blocks") << "listsAndCodeBlocks.md";
402}
403
404void tst_QTextMarkdownWriter::rewriteDocument()
405{
406 QFETCH(QString, inputFile);
407 QTextDocument doc;
408 QFile f(QFINDTESTDATA("data/" + inputFile));
409 QVERIFY(f.open(QFile::ReadOnly | QIODevice::Text));
410 QString orig = QString::fromUtf8(str: f.readAll());
411 f.close();
412 doc.setMarkdown(markdown: orig);
413 QString md = doc.toMarkdown();
414
415#ifdef DEBUG_WRITE_OUTPUT
416 QFile out("/tmp/rewrite-" + inputFile);
417 out.open(QFile::WriteOnly);
418 out.write(md.toUtf8());
419 out.close();
420#endif
421
422 QCOMPARE(md, orig);
423}
424
425void tst_QTextMarkdownWriter::fromHtml_data()
426{
427 QTest::addColumn<QString>(name: "expectedInput");
428 QTest::addColumn<QString>(name: "expectedOutput");
429
430 QTest::newRow(dataTag: "long URL") <<
431 "<span style=\"font-style:italic;\">https://www.example.com/dir/subdir/subsubdir/subsubsubdir/subsubsubsubdir/subsubsubsubsubdir/</span>" <<
432 "*https://www.example.com/dir/subdir/subsubdir/subsubsubdir/subsubsubsubdir/subsubsubsubsubdir/*\n\n";
433 QTest::newRow(dataTag: "non-emphasis inline asterisk") << "3 * 4" << "3 * 4\n\n";
434 QTest::newRow(dataTag: "arithmetic") << "(2 * a * x + b)^2 = b^2 - 4 * a * c" << "(2 * a * x + b)^2 = b^2 - 4 * a * c\n\n";
435 QTest::newRow(dataTag: "escaped asterisk after newline") <<
436 "The first sentence of this paragraph holds 80 characters, then there's a star. * This is wrapped, but is <em>not</em> a bullet point." <<
437 "The first sentence of this paragraph holds 80 characters, then there's a star.\n\\* This is wrapped, but is *not* a bullet point.\n\n";
438 QTest::newRow(dataTag: "escaped plus after newline") <<
439 "The first sentence of this paragraph holds 80 characters, then there's a plus. + This is wrapped, but is <em>not</em> a bullet point." <<
440 "The first sentence of this paragraph holds 80 characters, then there's a plus.\n\\+ This is wrapped, but is *not* a bullet point.\n\n";
441 QTest::newRow(dataTag: "escaped hyphen after newline") <<
442 "The first sentence of this paragraph holds 80 characters, then there's a minus. - This is wrapped, but is <em>not</em> a bullet point." <<
443 "The first sentence of this paragraph holds 80 characters, then there's a minus.\n\\- This is wrapped, but is *not* a bullet point.\n\n";
444 QTest::newRow(dataTag: "list items with indented continuations") <<
445 "<ul><li>bullet<p>continuation paragraph</p></li><li>another bullet<br/>continuation line</li></ul>" <<
446 "- bullet\n\n continuation paragraph\n\n- another bullet\n continuation line\n";
447 QTest::newRow(dataTag: "nested list items with continuations") <<
448 "<ul><li>bullet<p>continuation paragraph</p></li><li>another bullet<br/>continuation line</li><ul><li>bullet<p>continuation paragraph</p></li><li>another bullet<br/>continuation line</li></ul></ul>" <<
449 "- bullet\n\n continuation paragraph\n\n- another bullet\n continuation line\n\n - bullet\n\n continuation paragraph\n\n - another bullet\n continuation line\n";
450 QTest::newRow(dataTag: "nested ordered list items with continuations") <<
451 "<ol><li>item<p>continuation paragraph</p></li><li>another item<br/>continuation line</li><ol><li>item<p>continuation paragraph</p></li><li>another item<br/>continuation line</li></ol><li>another</li><li>another</li></ol>" <<
452 "1. item\n\n continuation paragraph\n\n2. another item\n continuation line\n\n 1. item\n\n continuation paragraph\n\n 2. another item\n continuation line\n\n3. another\n4. another\n";
453 QTest::newRow(dataTag: "thematic break") <<
454 "something<hr/>something else" <<
455 "something\n\n- - -\nsomething else\n\n";
456 QTest::newRow(dataTag: "block quote") <<
457 "<p>In 1958, Mahatma Gandhi was quoted as follows:</p><blockquote>The Earth provides enough to satisfy every man's need but not for every man's greed.</blockquote>" <<
458 "In 1958, Mahatma Gandhi was quoted as follows:\n\n> The Earth provides enough to satisfy every man's need but not for every man's\n> greed.\n\n";
459 QTest::newRow(dataTag: "image") <<
460 "<img src=\"/url\" alt=\"foo\" title=\"title\"/>" <<
461 "![foo](/url \"title\")\n\n";
462 QTest::newRow(dataTag: "code") <<
463 "<pre class=\"language-pseudocode\">\n#include \"foo.h\"\n\nblock {\n statement();\n}\n\n</pre>" <<
464 "```pseudocode\n#include \"foo.h\"\n\nblock {\n statement();\n}\n```\n\n";
465 // TODO
466// QTest::newRow("escaped number and paren after double newline") <<
467// "<p>(The first sentence of this paragraph is a line, the next paragraph has a number</p>13) but that's not part of an ordered list" <<
468// "(The first sentence of this paragraph is a line, the next paragraph has a number\n\n13\\) but that's not part of an ordered list\n\n";
469// QTest::newRow("preformats with embedded backticks") <<
470// "<pre>none `one` ``two``</pre><pre>```three``` ````four````</pre>plain" <<
471// "``` none `one` ``two`` ```\n\n````` ```three``` ````four```` `````\n\nplain\n\n";
472}
473
474void tst_QTextMarkdownWriter::fromHtml()
475{
476 QFETCH(QString, expectedInput);
477 QFETCH(QString, expectedOutput);
478
479 document->setHtml(expectedInput);
480 QString output = documentToUnixMarkdown();
481
482#ifdef DEBUG_WRITE_OUTPUT
483 {
484 QFile out("/tmp/" + QLatin1String(QTest::currentDataTag()) + ".md");
485 out.open(QFile::WriteOnly);
486 out.write(output.toUtf8());
487 out.close();
488 }
489#endif
490
491 QCOMPARE(output, expectedOutput);
492}
493
494QString tst_QTextMarkdownWriter::documentToUnixMarkdown()
495{
496 QString ret;
497 QTextStream ts(&ret, QIODevice::WriteOnly);
498 QTextMarkdownWriter writer(ts, QTextDocument::MarkdownDialectGitHub);
499 writer.writeAll(document);
500 return ret;
501}
502
503QTEST_MAIN(tst_QTextMarkdownWriter)
504#include "tst_qtextmarkdownwriter.moc"
505

source code of qtbase/tests/auto/gui/text/qtextmarkdownwriter/tst_qtextmarkdownwriter.cpp