From 4757ac11dc99451b0ec103a7e442d582df8b28bf Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Thu, 8 May 2025 14:50:24 -0400 Subject: [PATCH] Allow striking through text with ~~ Similar to spoilers, some Markdown flavors like GitHub's or Discord's allow you to strike through text with ~~. Normally, the only way to do this on most Matrix clients (including Element) is surrounding text with tags. As expected, no one knows how to do this. So now NeoChat supports the ~~ syntax. We can reuse the existing spoiler parser for this after making it generic. I have added new test cases for this syntax. This does not affect the quick format bar yet. --- autotests/texthandlertest.cpp | 28 +++++++----- src/libneochat/texthandler.cpp | 78 +++++++++++++++++++--------------- 2 files changed, 61 insertions(+), 45 deletions(-) diff --git a/autotests/texthandlertest.cpp b/autotests/texthandlertest.cpp index 69cb7cf75..7f2148304 100644 --- a/autotests/texthandlertest.cpp +++ b/autotests/texthandlertest.cpp @@ -43,8 +43,8 @@ private Q_SLOTS: void sendCustomEmoji(); void sendCustomEmojiCode_data(); void sendCustomEmojiCode(); - void sendSpoilerTags_data(); - void sendSpoilerTags(); + void sendCustomTags_data(); + void sendCustomTags(); void receiveSpacelessSelfClosingTag(); void receiveStripReply(); @@ -251,20 +251,28 @@ void TextHandlerTest::sendCustomEmojiCode() QCOMPARE(testTextHandler.handleSendText(), testOutputString); } -void TextHandlerTest::sendSpoilerTags_data() +void TextHandlerTest::sendCustomTags_data() { QTest::addColumn("testInputString"); QTest::addColumn("testOutputString"); - QTest::newRow("incomplete") << u"||test"_s << u"||test"_s; - QTest::newRow("complete") << u"||test||"_s << u"test"_s; - QTest::newRow("multiple") << u"||apple||banana||pear||"_s << u"applebananapear"_s; - QTest::newRow("inside code block") << u"```||apple||```"_s << u"||apple||"_s; - QTest::newRow("outside code block") << u"||apple|| ```||banana||``` ||pear||"_s - << u"apple ||banana|| pear"_s; + // spoiler + QTest::newRow("incomplete spoiler") << u"||test"_s << u"||test"_s; + QTest::newRow("complete spoiler") << u"||test||"_s << u"test"_s; + QTest::newRow("multiple spoiler") << u"||apple||banana||pear||"_s << u"applebananapear"_s; + QTest::newRow("inside code block spoiler") << u"```||apple||```"_s << u"||apple||"_s; + QTest::newRow("outside code block spoiler") << u"||apple|| ```||banana||``` ||pear||"_s + << u"apple ||banana|| pear"_s; + + // strikethrough + QTest::newRow("incomplete strikethrough") << u"~~test"_s << u"~~test"_s; + QTest::newRow("complete strikethrough") << u"~~test~~"_s << u"test"_s; + QTest::newRow("inside code block strikethrough") << u"```~~apple~~```"_s << u"~~apple~~"_s; + QTest::newRow("outside code block strikethrough") << u"~~apple~~ ```~~banana~~``` ~~pear~~"_s + << u"apple ~~banana~~ pear"_s; } -void TextHandlerTest::sendSpoilerTags() +void TextHandlerTest::sendCustomTags() { QFETCH(QString, testInputString); QFETCH(QString, testOutputString); diff --git a/src/libneochat/texthandler.cpp b/src/libneochat/texthandler.cpp index 6525fb8e7..9ac878209 100644 --- a/src/libneochat/texthandler.cpp +++ b/src/libneochat/texthandler.cpp @@ -706,45 +706,53 @@ QString TextHandler::customMarkdownToHtml(const QString &stringIn) { QString buffer = stringIn; - qsizetype beginCodeBlockTag = buffer.indexOf(u""_s); - qsizetype endCodeBlockTag = buffer.indexOf(u""_s, beginCodeBlockTag + 1); + const auto processSyntax = [&buffer](const QString &syntax, const QString &beginTag, const QString &endTag) { + qsizetype beginCodeBlockTag = buffer.indexOf(u""_s); + qsizetype endCodeBlockTag = buffer.indexOf(u""_s, beginCodeBlockTag + 1); - // Indesx to search from - qsizetype lastPos = 0; - while (true) { - const qsizetype pos = buffer.indexOf(u"||"_s, lastPos); - if (pos == -1) { - break; + // Index to search from + qsizetype lastPos = 0; + while (true) { + const qsizetype pos = buffer.indexOf(syntax, lastPos); + if (pos == -1) { + break; + } + + // If we're inside a code block, ignore and move the search past the code block + const bool validCodeBlock = beginCodeBlockTag != -1 && endCodeBlockTag != -1; + if (validCodeBlock && pos > beginCodeBlockTag && pos < endCodeBlockTag) { + lastPos = endCodeBlockTag + 5; + continue; + } + + qsizetype nextPos = buffer.indexOf(syntax, pos + 1); + if (nextPos == -1) { + break; + } + + // Replace the beginning syntax + buffer.replace(pos, 2, beginTag); + + // Update positions and re-search since the underlying text buffer changed + nextPos = buffer.indexOf(syntax, pos + 1); + beginCodeBlockTag = buffer.indexOf(u""_s, pos + 1); + endCodeBlockTag = buffer.indexOf(u""_s, beginCodeBlockTag + 1); + + // Now replace the end syntax + buffer.replace(nextPos, 2, endTag); + + // Move the search pointer past this point. + // Not technically needed in most cases since we replaced the original tag, but needed for code blocks + // which still have the characters. + lastPos = nextPos + 2; } + }; - // If we're inside of a code block, ignore and move the search past the code block - const bool validCodeBlock = beginCodeBlockTag != -1 && endCodeBlockTag != -1; - if (validCodeBlock && pos > beginCodeBlockTag && pos < endCodeBlockTag) { - lastPos = endCodeBlockTag + 5; - continue; - } + // spoilers + processSyntax(u"||"_s, u""_s, u""_s); - qsizetype nextPos = buffer.indexOf(u"||"_s, pos + 1); - if (nextPos == -1) { - break; - } - - // Replace the begin || - buffer.replace(pos, 2, QStringLiteral("")); - - // Update positions and re-search since the underlying text buffer changed - nextPos = buffer.indexOf(u"||"_s, pos + 1); - beginCodeBlockTag = buffer.indexOf(u""_s, pos + 1); - endCodeBlockTag = buffer.indexOf(u""_s, beginCodeBlockTag + 1); - - // Now replace the end || - buffer.replace(nextPos, 2, QStringLiteral("")); - - // Move the search pointer past this point. - // Not technically needed in most cases since we replaced the original tag, but needed for code blocks - // which still have the characters. - lastPos = nextPos + 2; - } + // strikethrough + processSyntax(u"~~"_s, u""_s, u""_s); return buffer; }