diff options
Diffstat (limited to 'tests/auto/gui/text/qtextmarkdownwriter')
11 files changed, 673 insertions, 72 deletions
diff --git a/tests/auto/gui/text/qtextmarkdownwriter/BLACKLIST b/tests/auto/gui/text/qtextmarkdownwriter/BLACKLIST deleted file mode 100644 index 12cb21f40a..0000000000 --- a/tests/auto/gui/text/qtextmarkdownwriter/BLACKLIST +++ /dev/null @@ -1,2 +0,0 @@ -# QTBUG-89819 -ci b2qt 32bit diff --git a/tests/auto/gui/text/qtextmarkdownwriter/CMakeLists.txt b/tests/auto/gui/text/qtextmarkdownwriter/CMakeLists.txt index d8815c64f6..0cdf1d9225 100644 --- a/tests/auto/gui/text/qtextmarkdownwriter/CMakeLists.txt +++ b/tests/auto/gui/text/qtextmarkdownwriter/CMakeLists.txt @@ -1,19 +1,26 @@ -# Generated from qtextmarkdownwriter.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## tst_qtextmarkdownwriter Test: ##################################################################### +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_qtextmarkdownwriter LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + # Collect test data -list(APPEND test_data "data/example.md") -list(APPEND test_data "data/blockquotes.md") +file(GLOB_RECURSE test_data + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + data/* +) qt_internal_add_test(tst_qtextmarkdownwriter SOURCES tst_qtextmarkdownwriter.cpp - DEFINES - SRCDIR=\\\"${CMAKE_CURRENT_SOURCE_DIR}\\\" - PUBLIC_LIBRARIES + LIBRARIES Qt::CorePrivate Qt::Gui Qt::GuiPrivate diff --git a/tests/auto/gui/text/qtextmarkdownwriter/data/blockquotes.md b/tests/auto/gui/text/qtextmarkdownwriter/data/blockquotes.md index 702ccef134..8e605ef7e6 100644 --- a/tests/auto/gui/text/qtextmarkdownwriter/data/blockquotes.md +++ b/tests/auto/gui/text/qtextmarkdownwriter/data/blockquotes.md @@ -8,18 +8,17 @@ MacFarlane writes: > What distinguishes Markdown from many other lightweight markup syntaxes, > which are often easier to write, is its readability. As Gruber writes: - +> > > The overriding design goal for Markdown's formatting syntax is to make it > > as readable as possible. The idea is that a Markdown-formatted document should > > be publishable as-is, as plain text, without looking like it's been marked up > > with tags or formatting instructions. ( -> > [http://daringfireball.net/projects/markdown/](http://daringfireball.net/projects/markdown/) -> > ) - +> > <http://daringfireball.net/projects/markdown/> ) +> > The point can be illustrated by comparing a sample of AsciiDoc with an > equivalent sample of Markdown. Here is a sample of AsciiDoc from the AsciiDoc > manual: - +> > ```AsciiDoc > 1. List item one. > + @@ -50,6 +49,7 @@ Now let's have an indented code block: } and end with a fenced code block: + ~~~pseudocode #include <something.h> #include <else.h> diff --git a/tests/auto/gui/text/qtextmarkdownwriter/data/blockquotesWithLists.md b/tests/auto/gui/text/qtextmarkdownwriter/data/blockquotesWithLists.md new file mode 100644 index 0000000000..1728889adc --- /dev/null +++ b/tests/auto/gui/text/qtextmarkdownwriter/data/blockquotesWithLists.md @@ -0,0 +1,14 @@ +What if we have a quotation containing a list? + +> First some quoted text, and then a list: +> +> - one +> - two is longer and has enough words to form a paragraph with text continuing +> onto the next line +> +> enough of that, let's try a numbered list +> +> 1. List item one +> 2. List item two is longer and has enough words to form a paragraph with +> text continuing onto the next line. +>
\ No newline at end of file diff --git a/tests/auto/gui/text/qtextmarkdownwriter/data/example.md b/tests/auto/gui/text/qtextmarkdownwriter/data/example.md index a9a157f25a..8fdad207ae 100644 --- a/tests/auto/gui/text/qtextmarkdownwriter/data/example.md +++ b/tests/auto/gui/text/qtextmarkdownwriter/data/example.md @@ -40,7 +40,7 @@ numerals in the same list structure: 1. Introduction 2. Qt Tools 1) Qt Assistant - 2) Qt Designer + 2) Qt Widgets Designer 1. Form Editor 2. Component Architecture 3) Qt Linguist @@ -70,7 +70,7 @@ column spans, text formatting within cells, and size constraints for columns. |-------------|------------------------------------|---------------------------|-------------------------| |9:00 - 11:00 |Introduction to Qt ||| |11:00 - 13:00|Using qmake |Object-oriented Programming|Layouts in Qt | -|13:00 - 15:00|Qt Designer Tutorial |Extreme Programming |Writing Custom Styles | +|13:00 - 15:00|Qt Widgets Designer Tutorial |Extreme Programming |Writing Custom Styles | |15:00 - 17:00|Qt Linguist and Internationalization|Test-Driven Development | | *Try adding text to the cells in the table and experiment with the alignment of @@ -83,7 +83,7 @@ is used extensively in [Qt Assistant](http://doc.qt.io/qt-5/qtassistant-index.html). Hyperlinks are automatically created when an HTML file is imported into an editor. Since the rich text framework supports hyperlinks natively, they can also be created -programatically. +programmatically. ## Undo and Redo diff --git a/tests/auto/gui/text/qtextmarkdownwriter/data/links.md b/tests/auto/gui/text/qtextmarkdownwriter/data/links.md index 33cdb2b3ab..c9aae80c67 100644 --- a/tests/auto/gui/text/qtextmarkdownwriter/data/links.md +++ b/tests/auto/gui/text/qtextmarkdownwriter/data/links.md @@ -23,3 +23,6 @@ title") * [link](/url "title title title") * nonlink + +Qt has the <https://qt.io> site + diff --git a/tests/auto/gui/text/qtextmarkdownwriter/data/listItemWithBlockquote.md b/tests/auto/gui/text/qtextmarkdownwriter/data/listItemWithBlockquote.md new file mode 100644 index 0000000000..c417125fea --- /dev/null +++ b/tests/auto/gui/text/qtextmarkdownwriter/data/listItemWithBlockquote.md @@ -0,0 +1,6 @@ +What if we have a list item containing a block quote? + +- one +- > two is longer and has enough words to form a paragraph with text continuing + > onto the next line + diff --git a/tests/auto/gui/text/qtextmarkdownwriter/data/listsAndCodeBlocks.md b/tests/auto/gui/text/qtextmarkdownwriter/data/listsAndCodeBlocks.md index 54e3f25afa..b3539167ab 100644 --- a/tests/auto/gui/text/qtextmarkdownwriter/data/listsAndCodeBlocks.md +++ b/tests/auto/gui/text/qtextmarkdownwriter/data/listsAndCodeBlocks.md @@ -22,3 +22,10 @@ - still didn't fix it, expecting a breakthrough any day now - some sort of miracle - profit! +- Alternatively we can have a non-indented fenced code block under a list item: + +```qml +import QtQuick +Text { text: "hello world" } +``` +- but that means the code block is not part of the list item. diff --git a/tests/auto/gui/text/qtextmarkdownwriter/data/longHeadings.md b/tests/auto/gui/text/qtextmarkdownwriter/data/longHeadings.md new file mode 100644 index 0000000000..72692b4845 --- /dev/null +++ b/tests/auto/gui/text/qtextmarkdownwriter/data/longHeadings.md @@ -0,0 +1,9 @@ +# The quick brown fox jumped over the lazy dog while the cat played the fiddle and the cow jumped over the moon + +Hey diddle diddle + +## This document has a verbose subheading too, which we do not expect to wrap in the output + +Qt can write it right. Long text here in this paragraph will actually wrap, +even though its heading doesn't. + diff --git a/tests/auto/gui/text/qtextmarkdownwriter/data/yaml.md b/tests/auto/gui/text/qtextmarkdownwriter/data/yaml.md new file mode 100644 index 0000000000..41303a0187 --- /dev/null +++ b/tests/auto/gui/text/qtextmarkdownwriter/data/yaml.md @@ -0,0 +1,11 @@ +--- +name: "Venus" +discoverer: "Galileo Galilei" +title: "A description of the planet Venus" +keywords: + - planets + - solar system + - astronomy +--- +*Venus* is the second planet from the Sun, orbiting it every 224.7 Earth days. + diff --git a/tests/auto/gui/text/qtextmarkdownwriter/tst_qtextmarkdownwriter.cpp b/tests/auto/gui/text/qtextmarkdownwriter/tst_qtextmarkdownwriter.cpp index bc6ee81ad4..0d261bc27e 100644 --- a/tests/auto/gui/text/qtextmarkdownwriter/tst_qtextmarkdownwriter.cpp +++ b/tests/auto/gui/text/qtextmarkdownwriter/tst_qtextmarkdownwriter.cpp @@ -1,30 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2019 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <QTest> #include <QTextDocument> @@ -34,9 +9,13 @@ #include <QTextTable> #include <QBuffer> #include <QDebug> +#include <QFontInfo> +#include <QLoggingCategory> #include <private/qtextmarkdownwriter_p.h> +Q_LOGGING_CATEGORY(lcTests, "qt.text.tests") + // #define DEBUG_WRITE_OUTPUT class tst_QTextMarkdownWriter : public QObject @@ -51,20 +30,35 @@ private slots: void testWriteParagraph(); void testWriteList(); void testWriteEmptyList(); + void testWriteCheckboxListItemEndingWithCode(); void testWriteNestedBulletLists_data(); void testWriteNestedBulletLists(); void testWriteNestedNumericLists(); + void testWriteNumericListWithStart(); void testWriteTable(); + void frontMatter(); + void charFormatWrapping_data(); + void charFormatWrapping(); + void charFormat_data(); + void charFormat(); void rewriteDocument_data(); void rewriteDocument(); void fromHtml_data(); void fromHtml(); + void fromPlainTextAndBack_data(); + void fromPlainTextAndBack(); + void escapeSpecialCharacters_data(); + void escapeSpecialCharacters(); private: + bool isMainFontFixed(); + bool isFixedFontProportional(); QString documentToUnixMarkdown(); private: QTextDocument *document; + QFont m_monoFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); + QFont m_defaultFont; }; void tst_QTextMarkdownWriter::init() @@ -77,10 +71,39 @@ void tst_QTextMarkdownWriter::cleanup() delete document; } +bool tst_QTextMarkdownWriter::isMainFontFixed() +{ + bool ret = QFontInfo(QGuiApplication::font()).fixedPitch(); + if (ret) { + qCWarning(lcTests) << "QFontDatabase::GeneralFont is monospaced: markdown writing is likely to use too many backticks" + << QFontDatabase::systemFont(QFontDatabase::GeneralFont); + } + return ret; +} + +bool tst_QTextMarkdownWriter::isFixedFontProportional() +{ + bool ret = !QFontInfo(QFontDatabase::systemFont(QFontDatabase::FixedFont)).fixedPitch(); + if (ret) { + qCWarning(lcTests) << "QFontDatabase::FixedFont is NOT monospaced: markdown writing is likely to use too few backticks" + << QFontDatabase::systemFont(QFontDatabase::FixedFont); + } + return ret; +} + +QString tst_QTextMarkdownWriter::documentToUnixMarkdown() +{ + QString ret; + QTextStream ts(&ret, QIODevice::WriteOnly); + QTextMarkdownWriter writer(ts, QTextDocument::MarkdownDialectGitHub); + writer.writeAll(document); + return ret; +} + void tst_QTextMarkdownWriter::testWriteParagraph_data() { QTest::addColumn<QString>("input"); - QTest::addColumn<QString>("output"); + QTest::addColumn<QString>("expectedOutput"); QTest::newRow("empty") << "" << ""; @@ -103,12 +126,15 @@ void tst_QTextMarkdownWriter::testWriteParagraph_data() void tst_QTextMarkdownWriter::testWriteParagraph() { QFETCH(QString, input); - QFETCH(QString, output); + QFETCH(QString, expectedOutput); QTextCursor cursor(document); cursor.insertText(input); - QCOMPARE(documentToUnixMarkdown(), output); + const QString output = documentToUnixMarkdown(); + if (output != expectedOutput && isMainFontFixed()) + QEXPECT_FAIL("", "fixed-pitch main font (QTBUG-103484)", Continue); + QCOMPARE(output, expectedOutput); } void tst_QTextMarkdownWriter::testWriteList() @@ -121,8 +147,11 @@ void tst_QTextMarkdownWriter::testWriteList() cursor.insertText("ListItem 2"); list->add(cursor.block()); - QCOMPARE(documentToUnixMarkdown(), QString::fromLatin1( - "- ListItem 1\n- ListItem 2\n")); + const QString output = documentToUnixMarkdown(); + const QString expected = QString::fromLatin1("- ListItem 1\n- ListItem 2\n"); + if (output != expected && isMainFontFixed()) + QEXPECT_FAIL("", "fixed-pitch main font (QTBUG-103484)", Continue); + QCOMPARE(output, expected); } void tst_QTextMarkdownWriter::testWriteEmptyList() @@ -133,6 +162,38 @@ void tst_QTextMarkdownWriter::testWriteEmptyList() QCOMPARE(documentToUnixMarkdown(), QString::fromLatin1("- \n")); } +void tst_QTextMarkdownWriter::testWriteCheckboxListItemEndingWithCode() +{ + QTextCursor cursor(document); + QTextList *list = cursor.createList(QTextListFormat::ListDisc); + cursor.insertText("Image.originalSize property (not necessary; PdfDocument.pagePointSize() substitutes)"); + list->add(cursor.block()); + { + auto fmt = cursor.block().blockFormat(); + fmt.setMarker(QTextBlockFormat::MarkerType::Unchecked); + cursor.setBlockFormat(fmt); + } + cursor.movePosition(QTextCursor::PreviousWord, QTextCursor::MoveAnchor, 2); + cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor); + cursor.movePosition(QTextCursor::PreviousWord, QTextCursor::KeepAnchor, 4); + QCOMPARE(cursor.selectedText(), QString::fromLatin1("PdfDocument.pagePointSize()")); + auto fmt = cursor.charFormat(); + fmt.setFontFixedPitch(true); + cursor.setCharFormat(fmt); + cursor.movePosition(QTextCursor::PreviousWord, QTextCursor::MoveAnchor, 5); + cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor); + cursor.movePosition(QTextCursor::PreviousWord, QTextCursor::KeepAnchor, 4); + QCOMPARE(cursor.selectedText(), QString::fromLatin1("Image.originalSize")); + cursor.setCharFormat(fmt); + + const QString output = documentToUnixMarkdown(); + const QString expected = QString::fromLatin1( + "- [ ] `Image.originalSize` property (not necessary; `PdfDocument.pagePointSize()`\n substitutes)\n"); + if (output != expected && isMainFontFixed()) + QEXPECT_FAIL("", "fixed-pitch main font (QTBUG-103484)", Continue); + QCOMPARE(output, expected); +} + void tst_QTextMarkdownWriter::testWriteNestedBulletLists_data() { QTest::addColumn<bool>("checkbox"); @@ -212,7 +273,7 @@ void tst_QTextMarkdownWriter::testWriteNestedBulletLists() cursor.insertText("continuation"); } - QString output = documentToUnixMarkdown(); + const QString output = documentToUnixMarkdown(); #ifdef DEBUG_WRITE_OUTPUT { QFile out("/tmp/" + QLatin1String(QTest::currentDataTag()) + ".md"); @@ -221,7 +282,9 @@ void tst_QTextMarkdownWriter::testWriteNestedBulletLists() out.close(); } #endif - QCOMPARE(documentToUnixMarkdown(), expectedOutput); + if (output != expectedOutput && isMainFontFixed()) + QEXPECT_FAIL("", "fixed-pitch main font (QTBUG-103484)", Continue); + QCOMPARE(output, expectedOutput); } void tst_QTextMarkdownWriter::testWriteNestedNumericLists() @@ -233,6 +296,7 @@ void tst_QTextMarkdownWriter::testWriteNestedNumericLists() list1->add(cursor.block()); QTextListFormat fmt2; + // Alpha "numbering" is not supported in markdown, so we'll actually get decimal. fmt2.setStyle(QTextListFormat::ListLowerAlpha); fmt2.setNumberSuffix(QLatin1String(")")); fmt2.setIndent(2); @@ -253,9 +317,116 @@ void tst_QTextMarkdownWriter::testWriteNestedNumericLists() cursor.insertText("ListItem 5"); list2->add(cursor.block()); - // There's no QTextList API to set the starting number so we hard-coded all lists to start at 1 (QTBUG-65384) - QCOMPARE(documentToUnixMarkdown(), QString::fromLatin1( - "1. ListItem 1\n 1) ListItem 2\n 1. ListItem 3\n2. ListItem 4\n 2) ListItem 5\n")); + const QString output = documentToUnixMarkdown(); + + #ifdef DEBUG_WRITE_OUTPUT + { + QFile out(QDir::temp().filePath(QLatin1String(QTest::currentTestFunction()) + ".md")); + out.open(QFile::WriteOnly); + out.write(output.toUtf8()); + out.close(); + } + { + QFile out(QDir::temp().filePath(QLatin1String(QTest::currentTestFunction()) + ".html")); + out.open(QFile::WriteOnly); + out.write(document->toHtml().toUtf8()); + out.close(); + } +#endif + + // While we can set the start index for a block, if list items intersect each other, they will + // still use the list numbering. + const QString expected = QString::fromLatin1( + "1. ListItem 1\n 1) ListItem 2\n 1. ListItem 3\n2. ListItem 4\n 2) ListItem 5\n"); + if (output != expected && isMainFontFixed()) + QEXPECT_FAIL("", "fixed-pitch main font (QTBUG-103484)", Continue); + QCOMPARE(output, expected); +} + +void tst_QTextMarkdownWriter::testWriteNumericListWithStart() +{ + QTextCursor cursor(document); + + // The first list will start at 2. + QTextListFormat fmt1; + fmt1.setStyle(QTextListFormat::ListDecimal); + fmt1.setStart(2); + QTextList *list1 = cursor.createList(fmt1); + cursor.insertText("ListItem 1"); + list1->add(cursor.block()); + + // This list uses the default start (1) again. + QTextListFormat fmt2; + // Alpha "numbering" is not supported in markdown, so we'll actually get decimal. + fmt2.setStyle(QTextListFormat::ListLowerAlpha); + fmt2.setNumberSuffix(QLatin1String(")")); + fmt2.setIndent(2); + QTextList *list2 = cursor.insertList(fmt2); + cursor.insertText("ListItem 2"); + + // Negative list numbers are disallowed by most Markdown implementations. This list will start + // at 1 for that reason. + QTextListFormat fmt3; + fmt3.setStyle(QTextListFormat::ListDecimal); + fmt3.setIndent(3); + fmt3.setStart(-1); + cursor.insertList(fmt3); + cursor.insertText("ListItem 3"); + + // Continuing list1, so the second item will have the number 3. + cursor.insertBlock(); + cursor.insertText("ListItem 4"); + list1->add(cursor.block()); + + // This will look out of place: it's in a different position than its list would suggest. + // Generates invalid markdown numbering (OK for humans, but md4c will parse it differently than we "meant"). + // TODO QTBUG-111707: the writer needs to add newlines, otherwise ListItem 5 becomes part of the text for ListItem 4. + cursor.insertBlock(); + cursor.insertText("ListItem 5"); + list2->add(cursor.block()); + + // 0 indexed lists are fine. + QTextListFormat fmt4; + fmt4.setStyle(QTextListFormat::ListDecimal); + fmt4.setStart(0); + QTextList *list4 = cursor.insertList(fmt4); + cursor.insertText("SecondList Item 0"); + list4->add(cursor.block()); + + // Ensure list numbers are incremented properly. + cursor.insertBlock(); + cursor.insertText("SecondList Item 1"); + list4->add(cursor.block()); + + const QString output = documentToUnixMarkdown(); + const QString expected = QString::fromLatin1( + R"(2. ListItem 1 + 1) ListItem 2 + 1. ListItem 3 +3. ListItem 4 + 2) ListItem 5 +0. SecondList Item 0 +1. SecondList Item 1 +)"); + +#ifdef DEBUG_WRITE_OUTPUT + { + QFile out(QDir::temp().filePath(QLatin1String(QTest::currentTestFunction()) + ".md")); + out.open(QFile::WriteOnly); + out.write(output.toUtf8()); + out.close(); + } + { + QFile out(QDir::temp().filePath(QLatin1String(QTest::currentTestFunction()) + ".html")); + out.open(QFile::WriteOnly); + out.write(document->toHtml().toUtf8()); + out.close(); + } +#endif + + if (output != expected && isMainFontFixed()) + QEXPECT_FAIL("", "fixed-pitch main font (QTBUG-103484)", Continue); + QCOMPARE(output, expected); } void tst_QTextMarkdownWriter::testWriteTable() @@ -308,6 +479,8 @@ void tst_QTextMarkdownWriter::testWriteTable() QString expected = QString::fromLatin1( "\n|one |two |three|\n|------|----|-----|\n|alice |bob |carl |\n|dennis|eric|fiona|\n|gina | | |\n\n"); + if (md != expected && isMainFontFixed()) + QEXPECT_FAIL("", "fixed-pitch main font (QTBUG-103484)", Continue); QCOMPARE(md, expected); // create table with merged cells @@ -357,7 +530,221 @@ void tst_QTextMarkdownWriter::testWriteTable() } #endif - QCOMPARE(md, QString::fromLatin1("\n|a ||b|\n|-|-|-|\n|c|d ||\n|e|f| |\n\n")); + expected = QString::fromLatin1("\n|a ||b|\n|-|-|-|\n|c|d ||\n|e|f| |\n\n"); + if (md != expected && isMainFontFixed()) + QEXPECT_FAIL("", "fixed-pitch main font (QTBUG-103484)", Continue); + QCOMPARE(md, expected); +} + +void tst_QTextMarkdownWriter::frontMatter() +{ + QTextCursor cursor(document); + cursor.insertText("bar"); + document->setMetaInformation(QTextDocument::FrontMatter, "foo"); + + const QString output = documentToUnixMarkdown(); + const QString expectedOutput("---\nfoo\n---\nbar\n\n"); + if (output != expectedOutput && isMainFontFixed()) + QEXPECT_FAIL("", "fixed-pitch main font (QTBUG-103484)", Continue); + QCOMPARE(output, expectedOutput); +} + +void tst_QTextMarkdownWriter::charFormatWrapping_data() +{ + QTest::addColumn<QTextFormat::Property>("property"); + QTest::addColumn<QVariant>("propertyValue"); + QTest::addColumn<QString>("followingText"); + QTest::addColumn<QString>("expectedIndicator"); + + const QString spaced = " after"; + const QString unspaced = ", and some more after"; + + QTest::newRow("FontFixedPitch-spaced") + << QTextFormat::FontFixedPitch << QVariant(true) << spaced << "`"; + QTest::newRow("FontFixedPitch-unspaced") + << QTextFormat::FontFixedPitch << QVariant(true) << unspaced << "`"; + QTest::newRow("FontItalic") + << QTextFormat::FontItalic << QVariant(true) << spaced << "*"; + QTest::newRow("FontUnderline") + << QTextFormat::FontUnderline << QVariant(true) << spaced << "_"; + QTest::newRow("FontStrikeOut") + << QTextFormat::FontStrikeOut << QVariant(true) << spaced << "~~"; + QTest::newRow("FontWeight-spaced") + << QTextFormat::FontWeight << QVariant(700) << spaced << "**"; + QTest::newRow("FontWeight-unspaced") + << QTextFormat::FontWeight << QVariant(700) << unspaced << "**"; +} + +void tst_QTextMarkdownWriter::charFormatWrapping() // QTBUG-116927 +{ + QFETCH(QTextFormat::Property, property); + QFETCH(QVariant, propertyValue); + QFETCH(QString, expectedIndicator); + QFETCH(QString, followingText); + + const QString newLine("\n"); + QTextCursor cursor(document); + cursor.insertText("around sixty-four characters to go before some formatted words "); + QTextCharFormat fmt; + fmt.setProperty(property, propertyValue); + cursor.setCharFormat(fmt); + cursor.insertText("formatted text"); + + cursor.setCharFormat({}); + cursor.insertText(followingText); + qsizetype lastNewLineIndex = 100; + + for (int push = 0; push < 10; ++push) { + if (push > 0) { + cursor.movePosition(QTextCursor::StartOfBlock); + cursor.insertText("a"); + } + + const QString output = documentToUnixMarkdown().trimmed(); // get rid of trailing newlines + const auto nlIdx = output.indexOf(newLine); + qCDebug(lcTests) << "push" << push << ":" << output << "newline @" << nlIdx; + // we're always wrapping in this test: expect to find a newline + QCOMPARE_GT(nlIdx, 70); + // don't expect the newline to be more than one character to the right of where we found it last time + // i.e. if we already started breaking in the middle: "`formatted\ntext`", + // then we would not expect that prepending one more character would make it go + // back to breaking afterwards: "`formatted text`\n" (because then the line becomes longer than necessary) + QCOMPARE_LE(nlIdx, lastNewLineIndex + 1); + lastNewLineIndex = nlIdx; + const QString nextChars = output.sliced(nlIdx + newLine.size(), expectedIndicator.size()); + const auto startingIndicatorIdx = output.indexOf(expectedIndicator); + // the starting indicator always exists, except in case of font problems on some CI platforms + if (startingIndicatorIdx <= 0) + QSKIP("starting indicator not found, probably due to platform font problems (QTBUG-103484 etc.)"); + const auto endingIndicatorIdx = output.indexOf(expectedIndicator, startingIndicatorIdx + 5); + qCDebug(lcTests) << "next chars past newline" << nextChars + << "indicators @" << startingIndicatorIdx << endingIndicatorIdx; + // the closing indicator must exist + QCOMPARE_GT(endingIndicatorIdx, startingIndicatorIdx); + // don't start a new line with an ending indicator: + // we can have "**formatted\ntext**" or "**formatted text**\n" or "\n**formatted text**" + // but not "**formatted text\n**" + if (startingIndicatorIdx < nlIdx) + QCOMPARE_NE(nextChars, expectedIndicator); + } +} + +void tst_QTextMarkdownWriter::charFormat_data() +{ + QTest::addColumn<QTextFormat::Property>("property"); + QTest::addColumn<QVariant>("propertyValue"); + QTest::addColumn<QFont>("explicitFont"); + QTest::addColumn<QString>("expectedOutput"); + + const QTextFormat::Property NoProperty = QTextFormat::ObjectIndex; + + QTest::newRow("FontFixedPitch") + << QTextFormat::FontFixedPitch << QVariant(true) << m_defaultFont + << "before `formatted` after"; + if (!isFixedFontProportional()) { + // QTBUG-54623 QTBUG-75649 QTBUG-79900 QTBUG-103484 etc. + QTest::newRow("mono font") << NoProperty << QVariant() << m_monoFont + << "before `formatted` after"; + } + + { + QFont font; + font.setItalic(true); + QTest::newRow("italic font") + << NoProperty << QVariant() << font + << "before *formatted* after"; + } + QTest::newRow("FontItalic") + << QTextFormat::FontItalic << QVariant(true) << m_defaultFont + << "before *formatted* after"; + + { + QFont font; + font.setUnderline(true); + QTest::newRow("underline font") + << NoProperty << QVariant() << font + << "before _formatted_ after"; + } + QTest::newRow("FontUnderline") + << QTextFormat::FontUnderline << QVariant(true) << m_defaultFont + << "before _formatted_ after"; + + { + QFont font; + font.setStrikeOut(true); + QTest::newRow("strikeout font") + << NoProperty << QVariant() << font + << "before ~~formatted~~ after"; + } + QTest::newRow("FontStrikeOut") + << QTextFormat::FontStrikeOut << QVariant(true) << m_defaultFont + << "before ~~formatted~~ after"; + + { + QFont font; + font.setBold(true); + QTest::newRow("bold font") + << NoProperty << QVariant() << font + << "before **formatted** after"; + } + { + QFont font; + font.setWeight(QFont::Black); + QTest::newRow("black font") + << NoProperty << QVariant() << font + << "before **formatted** after"; + } + QTest::newRow("FontWeight") + << QTextFormat::FontWeight << QVariant(700) << m_defaultFont + << "before **formatted** after"; + + QTest::newRow("AnchorHref") + << QTextFormat::AnchorHref << QVariant("linky linky") << m_defaultFont + << "before [formatted](linky linky) after"; + + QTest::newRow("TextToolTip") // no effect without AnchorHref + << QTextFormat::TextToolTip << QVariant("such a tool") << m_defaultFont + << "before formatted after"; +} + +void tst_QTextMarkdownWriter::charFormat() +{ + if (isMainFontFixed()) + QSKIP("QTextMarkdownWriter would generate bogus backticks"); + + QFETCH(QTextFormat::Property, property); + QFETCH(QVariant, propertyValue); + QFETCH(QFont, explicitFont); + QFETCH(QString, expectedOutput); + + QTextCursor cursor(document); + cursor.insertText("before "); + + QTextCharFormat fmt; + if (explicitFont != m_defaultFont) + fmt.setFont(explicitFont); + if (property != QTextFormat::ObjectIndex) // != 0 + fmt.setProperty(property, propertyValue); + if (explicitFont == m_monoFont) { + QFontInfo fontInfo(fmt.font()); + qCDebug(lcTests) << "mono font" << explicitFont << "fontInfo fixedPitch" << fontInfo.fixedPitch() << "fmt fixedPitch" << fmt.fontFixedPitch(); + } + cursor.setCharFormat(fmt); + cursor.insertText("formatted"); + + cursor.setCharFormat({}); + cursor.insertText(" after"); + + const QString output = documentToUnixMarkdown(); +#ifdef DEBUG_WRITE_OUTPUT + { + QFile out(QDir::temp().filePath(QLatin1String(QTest::currentDataTag()) + ".md")); + out.open(QFile::WriteOnly); + out.write(output.toUtf8()); + out.close(); + } +#endif + QCOMPARE(output.trimmed(), expectedOutput); } void tst_QTextMarkdownWriter::rewriteDocument_data() @@ -365,11 +752,15 @@ void tst_QTextMarkdownWriter::rewriteDocument_data() QTest::addColumn<QString>("inputFile"); QTest::newRow("block quotes") << "blockquotes.md"; + QTest::newRow("block quotes with lists") << "blockquotesWithLists.md"; + // QTest::newRow("list item with block quote") << "listItemWithBlockquote.md"; // not supported for now QTest::newRow("example") << "example.md"; QTest::newRow("list items after headings") << "headingsAndLists.md"; QTest::newRow("word wrap") << "wordWrap.md"; QTest::newRow("links") << "links.md"; QTest::newRow("lists and code blocks") << "listsAndCodeBlocks.md"; + QTest::newRow("front matter") << "yaml.md"; + QTest::newRow("long headings") << "longHeadings.md"; } void tst_QTextMarkdownWriter::rewriteDocument() @@ -390,17 +781,19 @@ void tst_QTextMarkdownWriter::rewriteDocument() out.close(); #endif + if (md != orig && isMainFontFixed()) + QEXPECT_FAIL("", "fixed-pitch main font (QTBUG-103484)", Continue); QCOMPARE(md, orig); } void tst_QTextMarkdownWriter::fromHtml_data() { - QTest::addColumn<QString>("expectedInput"); + QTest::addColumn<QString>("input"); QTest::addColumn<QString>("expectedOutput"); QTest::newRow("long URL") << "<span style=\"font-style:italic;\">https://www.example.com/dir/subdir/subsubdir/subsubsubdir/subsubsubsubdir/subsubsubsubsubdir/</span>" << - "*https://www.example.com/dir/subdir/subsubdir/subsubsubdir/subsubsubsubdir/subsubsubsubsubdir/*\n\n"; + "\n*https://www.example.com/dir/subdir/subsubdir/subsubsubdir/subsubsubsubdir/subsubsubsubsubdir/*\n\n"; QTest::newRow("non-emphasis inline asterisk") << "3 * 4" << "3 * 4\n\n"; QTest::newRow("arithmetic") << "(2 * a * x + b)^2 = b^2 - 4 * a * c" << "(2 * a * x + b)^2 = b^2 - 4 * a * c\n\n"; QTest::newRow("escaped asterisk after newline") << @@ -432,22 +825,51 @@ void tst_QTextMarkdownWriter::fromHtml_data() "![foo](/url \"title\")\n\n"; QTest::newRow("code") << "<pre class=\"language-pseudocode\">\n#include \"foo.h\"\n\nblock {\n statement();\n}\n\n</pre>" << - "```pseudocode\n#include \"foo.h\"\n\nblock {\n statement();\n}\n```\n\n"; - // TODO -// QTest::newRow("escaped number and paren after double newline") << -// "<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" << -// "(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"; -// QTest::newRow("preformats with embedded backticks") << -// "<pre>none `one` ``two``</pre><pre>```three``` ````four````</pre>plain" << -// "``` none `one` ``two`` ```\n\n````` ```three``` ````four```` `````\n\nplain\n\n"; + "```pseudocode\n#include \"foo.h\"\n\nblock {\n statement();\n}\n\n```\n\n"; + QTest::newRow("escaped number and paren after single newline") << + "<p>(The first sentence of this paragraph is a line, next paragraph has a number 13) but that's not part of an ordered list</p>" << + "(The first sentence of this paragraph is a line, next paragraph has a number\n13\\) but that's not part of an ordered list\n\n"; + QTest::newRow("escaped number and paren after double newline") << + "<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" << + "(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"; + QTest::newRow("preformats with embedded backticks") << + "<pre>none `one` ``two``</pre>plain<pre>```three``` ````four````</pre>plain" << + "```\nnone `one` ``two``\n\n```\nplain\n\n```\n```three``` ````four````\n\n```\nplain\n\n"; + QTest::newRow("list items with and without checkboxes") << + "<ul><li>bullet</li><li class=\"unchecked\">unchecked item</li><li class=\"checked\">checked item</li></ul>" << + "- bullet\n- [ ] unchecked item\n- [x] checked item\n"; + QTest::newRow("table with backslash in cell") << // QTBUG-96051 + "<table><tr><td>1011011 [</td><td>1011100 backslash \\</td></tr></table>" << + "|1011011 [|1011100 backslash \\\\|"; + // https://spec.commonmark.org/0.31.2/#example-12 + // escaping punctuation is ok, but QTextMarkdownWriter currently doesn't do that (which is also ok) + QTest::newRow("punctuation") << + R"(<p>!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~</p>)" << + R"(!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~)"; + // https://spec.commonmark.org/0.31.2/#example-14 + QTest::newRow("backslash asterisk no emphasis") << // QTBUG-122083 + R"(\*no emphasis*)" << + R"(\\\*no emphasis*)"; + // https://spec.commonmark.org/0.31.2/#example-15 + QTest::newRow("backslash before emphasis") << + R"(\<em>emphasis</em>)" << + R"(\\*emphasis*)"; + // https://spec.commonmark.org/0.31.2/#example-20 + QTest::newRow("backslash-asterisk in autolink") << + R"(<p><a href="https://example.com?find=\\*">https://example.com?find=\*</a></p>)" << + R"(<https://example.com?find=\\*>)"; + // https://spec.commonmark.org/0.31.2/#example-24 + QTest::newRow("plus in fenced code lang") << + "<pre class=\"language-foo+bar\">foo</pre>" << + "```foo+bar\nfoo\n```"; } void tst_QTextMarkdownWriter::fromHtml() { - QFETCH(QString, expectedInput); + QFETCH(QString, input); QFETCH(QString, expectedOutput); - document->setHtml(expectedInput); + document->setHtml(input); QString output = documentToUnixMarkdown(); #ifdef DEBUG_WRITE_OUTPUT @@ -459,16 +881,140 @@ void tst_QTextMarkdownWriter::fromHtml() } #endif + output = output.trimmed(); + expectedOutput = expectedOutput.trimmed(); + if (output != expectedOutput && (isMainFontFixed() || isFixedFontProportional())) + QEXPECT_FAIL("", "fixed main font or proportional fixed font (QTBUG-103484)", Continue); QCOMPARE(output, expectedOutput); } -QString tst_QTextMarkdownWriter::documentToUnixMarkdown() +void tst_QTextMarkdownWriter::fromPlainTextAndBack_data() { - QString ret; - QTextStream ts(&ret, QIODevice::WriteOnly); - QTextMarkdownWriter writer(ts, QTextDocument::MarkdownDialectGitHub); - writer.writeAll(document); - return ret; + QTest::addColumn<QString>("input"); + QTest::addColumn<QString>("expectedMarkdown"); + + // tests to verify that fixing QTBUG-122083 is safe + QTest::newRow("single backslashes") << + R"(\ again: \ not esc: \* \-\-\ \*abc*)" << + R"(\\ again: \\ not esc: \\* \\-\\-\\ \\\*abc*)"; + // https://spec.commonmark.org/0.31.2/#example-12 + QTest::newRow("punctuation") << + R"(!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~)" << + R"(!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~)"; + // https://spec.commonmark.org/0.31.2/#example-13 + QTest::newRow("literal backslashes") << + QString(uR"(\→\A\a\ \3\φ\«)") << + "\\\\\u2192\\\\A\\\\a\\\\ \\\\3\\\\\u03C6\\\\\u00AB"; + // https://spec.commonmark.org/0.31.2/#example-14 + QTest::newRow("escape to avoid em") << + R"(*not emphasized*)" << + R"(\*not emphasized*)"; + QTest::newRow("escape to avoid html") << + R"(<br/> not a tag)" << + R"(\<br/> not a tag)"; + QTest::newRow("escape to avoid link") << + R"([not a link](/foo))" << + R"(\[not a link](/foo))"; + QTest::newRow("escape to avoid mono") << + R"(`not code`)" << + R"(\`not code`)"; + QTest::newRow("escape to avoid num list") << + R"(1. not a list)" << + R"(1\. not a list)"; + QTest::newRow("escape to avoid list") << + R"(* not a list)" << + R"(\* not a list)"; + QTest::newRow("escape to avoid heading") << + R"(# not a heading)" << + R"(\# not a heading)"; + QTest::newRow("escape to avoid reflink") << + R"([foo]: /url "not a reference")" << + R"(\[foo]: /url "not a reference")"; + QTest::newRow("escape to avoid entity") << + R"(ö not a character entity)" << + R"(\ö not a character entity)"; + // end of tests to verify that fixing QTBUG-122083 is safe + // (it's ok to add unrelated plain-to-markdown-to-plaintext cases later) +} + +void tst_QTextMarkdownWriter::fromPlainTextAndBack() +{ + QFETCH(QString, input); + QFETCH(QString, expectedMarkdown); + + document->setPlainText(input); + QString output = documentToUnixMarkdown(); + +#ifdef DEBUG_WRITE_OUTPUT + { + QFile out("/tmp/" + QLatin1String(QTest::currentDataTag()) + ".md"); + out.open(QFile::WriteOnly); + out.write(output.toUtf8()); + out.close(); + } +#endif + + output = output.trimmed(); + expectedMarkdown = expectedMarkdown.trimmed(); + if (output != expectedMarkdown && (isMainFontFixed() || isFixedFontProportional())) + QSKIP("", "fixed main font or proportional fixed font (QTBUG-103484)"); + QCOMPARE(output, expectedMarkdown); + QCOMPARE(document->toPlainText(), input); + document->setMarkdown(output); + QCOMPARE(document->toPlainText(), input); + if (document->blockCount() == 1) + QCOMPARE(document->firstBlock().text(), input); +} + +void tst_QTextMarkdownWriter::escapeSpecialCharacters_data() +{ + QTest::addColumn<QString>("input"); + QTest::addColumn<QString>("expectedOutput"); + + QTest::newRow("backslash") << "foo \\ bar \\\\ baz \\" << "foo \\\\ bar \\\\\\\\ baz \\\\"; + QTest::newRow("not emphasized") << "*normal* **normal too**" << "\\*normal* \\**normal too**"; + QTest::newRow("not code") << "`normal` `normal too`" << "\\`normal` \\`normal too`"; + QTest::newRow("code fence") << "```not a fence; ``` no risk here; ```not a fence" // TODO slightly inconsistent + << "\\```not a fence; ``` no risk here; \\```not a fence"; + QTest::newRow("not html") << "<p>not a tag: <br/> nope</p>" << "\\<p>not a tag: \\<br/> nope\\</p>"; + QTest::newRow("not a link") << "text [not a link](/foo)" << "text \\[not a link](/foo)"; + QTest::newRow("not a circle") << "* polaris" << "\\* polaris"; + QTest::newRow("not a square") << "+ groovy" << "\\+ groovy"; + QTest::newRow("not a bullet") << "- stayin alive" << "\\- stayin alive"; + QTest::newRow("arithmetic") << "1 + 2 - 3 * 4" << "1 + 2 - 3 * 4"; + QTest::newRow("not a list") << "1. not a list" << "1\\. not a list"; + QTest::newRow("not a list either") << "Jupiter and 10." << "Jupiter and 10."; + QTest::newRow("not a heading") << "# not a heading" << "\\# not a heading"; + QTest::newRow("a non-entity") << "ö not a character entity" << "\\ö not a character entity"; +} + +/*! \internal + If the user types into a Qt-based editor plain text that the + markdown parser would misinterpret, escape it when we save to markdown + to clarify that it's plain text. + https://spec.commonmark.org/0.31.2/#backslash-escapes +*/ +void tst_QTextMarkdownWriter::escapeSpecialCharacters() // QTBUG-96051, QTBUG-122083 +{ + QFETCH(QString, input); + QFETCH(QString, expectedOutput); + + document->setPlainText(input); + QString output = documentToUnixMarkdown(); + +#ifdef DEBUG_WRITE_OUTPUT + { + QFile out("/tmp/" + QLatin1String(QTest::currentDataTag()) + ".md"); + out.open(QFile::WriteOnly); + out.write(output.toUtf8()); + out.close(); + } +#endif + + output = output.trimmed(); + if (output != expectedOutput && (isMainFontFixed() || isFixedFontProportional())) + QEXPECT_FAIL("", "fixed main font or proportional fixed font (QTBUG-103484)", Continue); + QCOMPARE(output, expectedOutput); } QTEST_MAIN(tst_QTextMarkdownWriter) |