/* Разметка, реальные языковые определения, кеш и метрики текста без участия сетевого ядра. */ #include "../src/message_text.h" #include "../src/messagedelegate.h" #include "../../../lib/debug_config.h" #include #include class MessageTextTest : public QObject { Q_OBJECT private slots: void initTestCase() { debug_enable_console(true); debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DEBUG); } void parse_data() { QTest::addColumn("source"); QTest::addColumn("visible"); QTest::addColumn("styles"); QTest::addColumn("blocks"); QTest::newRow("plain") << "hello\nworld" << "hello\nworld" << 0 << 0; QTest::newRow("styles") << "**жирный** *наклонный* ~~удалённый~~" << "жирный наклонный удалённый" << 3 << 0; QTest::newRow("unclosed") << "**hello *world ~~bye" << "**hello *world ~~bye" << 0 << 0; QTest::newRow("no-multiline-style") << "**hello\nworld**" << "**hello\nworld**" << 0 << 0; QTest::newRow("empty-style") << "**** ** ** ~~~~" << "**** ** ** ~~~~" << 0 << 0; QTest::newRow("no-nesting") << "**hello *world* **" << "**hello world **" << 1 << 0; QTest::newRow("nested-literal") << "**hello *world***" << "**hello *world***" << 0 << 0; QTest::newRow("escapes") << R"(\*hello\* \\ ~~bye~~)" << "*hello* \\ bye" << 1 << 0; QTest::newRow("code") << "before\n```cpp\n int x;\n\t/* **literal** */\n\n```\nafter" << "before\n int x;\n\t/* **literal** */\n\nafter" << 0 << 1; QTest::newRow("empty-code") << "```\n```" << "" << 0 << 1; QTest::newRow("windows-newlines") << "```cpp\r\nint x;\r\n```" << "int x;" << 0 << 1; QTest::newRow("blank-code") << "```\n\n\n```" << "\n" << 0 << 1; QTest::newRow("two-blocks") << "```c\nint x;\n```\n**bold**\n```python\npass\n```" << "int x;\nbold\npass" << 1 << 2; QTest::newRow("unclosed-code") << "```cpp\nint x;" << "```cpp\nint x;" << 0 << 0; QTest::newRow("inline-fence") << "text ``` code ```" << "text ``` code ```" << 0 << 0; QTest::newRow("indented-fence") << " ```\nx\n ```" << " ```\nx\n ```" << 0 << 0; QTest::newRow("trailing-fence-text") << "```cpp extra\nx\n```" << "```cpp extra\nx\n```" << 0 << 0; QTest::newRow("no-word-markup") << "some*identifier* a**b**c" << "some*identifier* a**b**c" << 0 << 0; QTest::newRow("url-markers") << "https://example.com/*path*/ **https://example.org**" << "https://example.com/*path*/ https://example.org" << 1 << 0; } void parse() { QFETCH(QString, source); QFETCH(QString, visible); QFETCH(int, styles); QFETCH(int, blocks); const auto parsed = parseMessageText(source); QCOMPARE(parsed.text, visible); QCOMPARE(parsed.styles.size(), styles); QCOMPARE(parsed.code.size(), blocks); for (const auto& style : parsed.styles) { QVERIFY(style.start >= 0); QVERIFY(style.length > 0); QVERIFY(style.start + style.length <= parsed.text.size()); } } void stylePropertiesAndCodeLinks() { const auto parsed = parseMessageText("**bold** *italic* ~~strike~~\n```cpp\nhttps://example.com\n```\nhttps://example.org"); QCOMPARE(parsed.styles[0].format.fontWeight(), QFont::Bold); QVERIFY(parsed.styles[1].format.fontItalic()); QVERIFY(parsed.styles[2].format.fontStrikeOut()); QCOMPARE(parsed.links.size(), 1); QCOMPARE(parsed.links[0].url, QString("https://example.org")); QVERIFY(parsed.isCode(parsed.text.indexOf("https://example.com"))); QVERIFY(!parsed.isCode(parsed.links[0].start)); } void syntaxAndCache() { QStandardItemModel model(1, 1); const auto index = model.index(0, 0); const QString source = "```cpp\n/* comment\n continued */\nint x = 42;\n```"; model.setData(index, source, MsgTextRole); const auto parsed = formattedMessageText(index); QVERIFY(parsed); QCOMPARE(parsed->text, QString("/* comment\n continued */\nint x = 42;")); QVERIFY(!parsed->lightSyntax.isEmpty()); QVERIFY(!parsed->darkSyntax.isEmpty()); auto colorAt = [&](int position) { QColor color; for (const auto& token : parsed->lightSyntax) if (position >= token.start && position < token.start + token.length) color = token.format.foreground().color(); return color; }; QVERIFY(colorAt(0).isValid()); QCOMPARE(colorAt(0), colorAt(parsed->text.indexOf("continued"))); QVERIFY(colorAt(0) != colorAt(parsed->text.indexOf("int"))); QCOMPARE(formattedMessageText(index).data(), parsed.data()); QCOMPARE(index.data(MsgTextRole).toString(), source); model.setData(index, "```unknown_language\nint x;\n```", MsgTextRole); const auto unknown = formattedMessageText(index); QVERIFY(unknown); QVERIFY(unknown->lightSyntax.isEmpty()); model.setData(index, "```\nint x;\n```", MsgTextRole); const auto plainCode = formattedMessageText(index); QVERIFY(plainCode); QVERIFY(plainCode->lightSyntax.isEmpty()); model.setData(index, "```\n```", MsgTextRole); const auto emptyCode = formattedMessageText(index); QVERIFY(emptyCode); QTextLayout emptyLayout; emptyLayout.setFont(QApplication::font()); QVERIFY(layoutFormattedMessage(emptyLayout, *emptyCode, 200, false) > 0); QCOMPARE(emptyLayout.lineCount(), 1); QVERIFY(emptyCode->isCode(emptyLayout.lineAt(0).textStart())); model.setData(index, "**invitation**", MsgTextRole); model.setData(index, "utun://abc", MsgInviteLinkRole); QVERIFY(!formattedMessageText(index)); model.setData(index, QString(), MsgInviteLinkRole); model.setData(index, "poll", MsgContentTypeRole); QVERIFY(!formattedMessageText(index)); } void layoutPreservesTextAndThemeGeometry() { QStandardItemModel model(1, 1); const auto index = model.index(0, 0); model.setData(index, "**Текст 😀**\n```cpp\n\tint value = 123456789; // длинная строка\n\n```\n*конец*", MsgTextRole); const auto parsed = formattedMessageText(index); QVERIFY(parsed); const auto original = parsed->text; for (int width : {80, 200, 600}) { QTextLayout light, dark; light.setFont(QApplication::font()); dark.setFont(light.font()); const qreal height = layoutFormattedMessage(light, *parsed, width, false); QCOMPARE(layoutFormattedMessage(dark, *parsed, width, true), height); QCOMPARE(light.lineCount(), dark.lineCount()); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "formatted layout: width=%d lines=%d height=%.1f", width, light.lineCount(), height); QVERIFY(light.lineCount() >= 4); for (int i = 0; i < light.lineCount(); ++i) { QCOMPARE(light.lineAt(i).textStart(), dark.lineAt(i).textStart()); QCOMPARE(light.lineAt(i).naturalTextWidth(), dark.lineAt(i).naturalTextWidth()); QVERIFY(light.lineAt(i).y() + light.lineAt(i).height() <= height); } QCOMPARE(parsed->text, original); } DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "message formatting: real C++ tokens, multiline state, theme geometry and cache verified"); } }; QTEST_MAIN(MessageTextTest) #include "test_message_text.moc"