Newer
Older
// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
Konstantinos Sideris
committed
#include "Utils.h"
#include <QApplication>
#include <QCryptographicHash>
#include <QGuiApplication>
#include <QProcessEnvironment>
#include <QSettings>
#include <QTextDocument>
#include <QXmlStreamReader>
#include <mtx/responses/messages.hpp>
Konstantinos Sideris
committed
#include "Cache.h"
#include "ChatPage.h"
#include "EventAccessors.h"
#include "MatrixClient.h"
#include "timeline/Permissions.h"
template<class T, class Event>
static DescInfo
createDescriptionInfo(const Event &event, const QString &localUser, const QString &displayName)
const auto msg = std::get<T>(event);
const auto sender = QString::fromStdString(msg.sender);
const auto username = displayName;
const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts);
if (mtx::accessors::relations(event).reply_to())
body = utils::stripReplyFromBody(body);
return DescInfo{
QString::fromStdString(msg.event_id),
sender,
utils::messageDescription<T>(username, QString::fromStdString(body), sender == localUser),
utils::descriptiveTime(ts),
msg.origin_server_ts,
ts};
std::string
utils::stripReplyFromBody(const std::string &bodyi)
{
QString body = QString::fromStdString(bodyi);
if (body.startsWith(QLatin1String("> <"))) {
auto segments = body.split('\n');
while (!segments.isEmpty() && segments.begin()->startsWith('>'))
if (!segments.empty() && segments.first().isEmpty())
body = segments.join('\n');
}
body.replace(QLatin1String("@room"), QString::fromUtf8("@\u2060room"));
}
std::string
utils::stripReplyFromFormattedBody(const std::string &formatted_bodyi)
{
QString formatted_body = QString::fromStdString(formatted_bodyi);
static QRegularExpression replyRegex(QStringLiteral("<mx-reply>.*</mx-reply>"),
QRegularExpression::DotMatchesEverythingOption);
formatted_body.remove(replyRegex);
formatted_body.replace(QLatin1String("@room"), QString::fromUtf8("@\u2060room"));
}
utils::stripReplyFallbacks(const mtx::events::collections::TimelineEvents &event,
std::string id,
QString room_id_)
RelatedInfo related = {};
related.quoted_user = QString::fromStdString(mtx::accessors::sender(event));
related.related_event = std::move(id);
related.type = mtx::accessors::msg_type(event);
// get body, strip reply fallback, then transform the event to text, if it is a media event
// etc
related.quoted_body = QString::fromStdString(mtx::accessors::body(event));
related.quoted_body =
QString::fromStdString(stripReplyFromBody(related.quoted_body.toStdString()));
related.quoted_body = utils::getQuoteBody(related);
// get quoted body and strip reply fallback
related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event);
related.quoted_formatted_body = QString::fromStdString(
stripReplyFromFormattedBody(related.quoted_formatted_body.toStdString()));
related.room = room_id_;
QString
utils::localUser()
{
return QString::fromStdString(http::client()->user_id().to_string());
}
bool
utils::codepointIsEmoji(uint code)
{
// TODO: Be more precise here.
return (code >= 0x2600 && code <= 0x27bf) || (code >= 0x2b00 && code <= 0x2bff) ||
(code >= 0x1f000 && code <= 0x1faff) || code == 0x200d || code == 0xfe0f;
}
QString
utils::replaceEmoji(const QString &body)
{
QString fmtBody;
fmtBody.reserve(body.size());
QVector<uint> utf32_string = body.toUcs4();
bool insideFontBlock = false;
if (code == U'<')
insideTag = true;
else if (code == U'>')
insideTag = false;
if (!insideTag && utils::codepointIsEmoji(code)) {
if (!insideFontBlock) {
fmtBody += QStringLiteral("<font face=\"") % UserSettings::instance()->emojiFont() %
(UserSettings::instance()->enlargeEmojiOnlyMessages()
? QStringLiteral("\" size=\"4\">")
: QStringLiteral("\">"));
} else if (code == 0xfe0f) {
// BUG(Nico):
// Workaround https://bugreports.qt.io/browse/QTBUG-97401
// See also https://github.com/matrix-org/matrix-react-sdk/pull/1458/files
// Nheko bug: https://github.com/Nheko-Reborn/nheko/issues/439
continue;
}
} else {
if (insideFontBlock) {
fmtBody += QStringLiteral("</font>");
if (QChar::requiresSurrogates(code)) {
QChar emoji[] = {static_cast<ushort>(QChar::highSurrogate(code)),
static_cast<ushort>(QChar::lowSurrogate(code))};
fmtBody.append(emoji, 2);
} else {
fmtBody.append(QChar(static_cast<ushort>(code)));
}
}
if (insideFontBlock) {
fmtBody += QStringLiteral("</font>");
}
void
utils::setScaleFactor(float factor)
{
if (factor < 1 || factor > 3)
return;
settings.setValue(QStringLiteral("settings/scale_factor"), factor);
}
float
utils::scaleFactor()
{
return settings.value(QStringLiteral("settings/scale_factor"), -1).toFloat();
Konstantinos Sideris
committed
QString
utils::descriptiveTime(const QDateTime &then)
{
const auto now = QDateTime::currentDateTime();
const auto days = then.daysTo(now);
Konstantinos Sideris
committed
if (days == 0)
return QLocale::system().toString(then.time(), QLocale::ShortFormat);
else if (days < 2)
return QString(QCoreApplication::translate("descriptiveTime", "Yesterday"));
else if (days < 7)
return then.toString(QStringLiteral("dddd"));
Konstantinos Sideris
committed
return QLocale::system().toString(then.date(), QLocale::ShortFormat);
Konstantinos Sideris
committed
}
DescInfo
utils::getMessageDescription(const mtx::events::collections::TimelineEvents &event,
Konstantinos Sideris
committed
{
using Audio = mtx::events::RoomEvent<mtx::events::msg::Audio>;
using Emote = mtx::events::RoomEvent<mtx::events::msg::Emote>;
using File = mtx::events::RoomEvent<mtx::events::msg::File>;
using Image = mtx::events::RoomEvent<mtx::events::msg::Image>;
using Notice = mtx::events::RoomEvent<mtx::events::msg::Notice>;
using Text = mtx::events::RoomEvent<mtx::events::msg::Text>;
using Unknown = mtx::events::RoomEvent<mtx::events::msg::Unknown>;
using Video = mtx::events::RoomEvent<mtx::events::msg::Video>;
using ElementEffect = mtx::events::RoomEvent<mtx::events::msg::ElementEffect>;
using CallInvite = mtx::events::RoomEvent<mtx::events::voip::CallInvite>;
using CallAnswer = mtx::events::RoomEvent<mtx::events::voip::CallAnswer>;
using CallHangUp = mtx::events::RoomEvent<mtx::events::voip::CallHangUp>;
using CallReject = mtx::events::RoomEvent<mtx::events::voip::CallReject>;
using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>;
if (std::holds_alternative<Audio>(event)) {
return createDescriptionInfo<Audio>(event, localUser, displayName);
} else if (std::holds_alternative<Emote>(event)) {
return createDescriptionInfo<Emote>(event, localUser, displayName);
} else if (std::holds_alternative<File>(event)) {
return createDescriptionInfo<File>(event, localUser, displayName);
} else if (std::holds_alternative<Image>(event)) {
return createDescriptionInfo<Image>(event, localUser, displayName);
} else if (std::holds_alternative<Notice>(event)) {
return createDescriptionInfo<Notice>(event, localUser, displayName);
} else if (std::holds_alternative<Text>(event)) {
return createDescriptionInfo<Text>(event, localUser, displayName);
} else if (std::holds_alternative<Unknown>(event)) {
return createDescriptionInfo<Unknown>(event, localUser, displayName);
} else if (std::holds_alternative<Video>(event)) {
return createDescriptionInfo<Video>(event, localUser, displayName);
} else if (std::holds_alternative<ElementEffect>(event)) {
return createDescriptionInfo<ElementEffect>(event, localUser, displayName);
} else if (std::holds_alternative<CallInvite>(event)) {
return createDescriptionInfo<CallInvite>(event, localUser, displayName);
} else if (std::holds_alternative<CallAnswer>(event)) {
return createDescriptionInfo<CallAnswer>(event, localUser, displayName);
} else if (std::holds_alternative<CallHangUp>(event)) {
return createDescriptionInfo<CallHangUp>(event, localUser, displayName);
} else if (std::holds_alternative<CallReject>(event)) {
return createDescriptionInfo<CallReject>(event, localUser, displayName);
} else if (std::holds_alternative<mtx::events::Sticker>(event)) {
return createDescriptionInfo<mtx::events::Sticker>(event, localUser, displayName);
} else if (auto msg = std::get_if<Encrypted>(&event); msg != nullptr) {
const auto sender = QString::fromStdString(msg->sender);
const auto username = displayName;
const auto ts = QDateTime::fromMSecsSinceEpoch(msg->origin_server_ts);
DescInfo info;
info.userid = sender;
info.body = QStringLiteral(" %1").arg(
messageDescription<Encrypted>(username, QLatin1String(""), sender == localUser));
info.timestamp = msg->origin_server_ts;
info.descriptiveTime = utils::descriptiveTime(ts);
info.event_id = QString::fromStdString(msg->event_id);
info.datetime = ts;
return info;
}
Konstantinos Sideris
committed
Konstantinos Sideris
committed
}
QString
utils::firstChar(const QString &input)
{
for (auto const &c : input.toStdU32String()) {
if (QString::fromUcs4(&c, 1) != QStringLiteral("#"))
return QString::fromUcs4(&c, 1).toUpper();
}
return QString::fromUcs4(&input.toStdU32String().at(0), 1).toUpper();
constexpr static const char *units[] = {"B", "KiB", "MiB", "GiB", "TiB"};
constexpr static const int length = sizeof(units) / sizeof(units[0]);
int u = 0;
double size = static_cast<double>(bytes);
while (size >= 1024.0 && u < length) {
++u;
size /= 1024.0;
}
return QString::number(size, 'g', 4) + ' ' + units[u];
int
utils::levenshtein_distance(const std::string &s1, const std::string &s2)
{
const auto nlen = s1.size();
const auto hlen = s2.size();
if (hlen == 0)
return -1;
if (nlen == 1)
return (int)s2.find(s1);
for (size_t i = 0; i < nlen; ++i) {
std::vector<int> row2(1, (int)i + 1);
for (size_t j = 0; j < hlen; ++j) {
const int cost = s1[i] != s2[j];
row2.push_back(std::min(row1[j + 1] + 1, std::min(row2[j] + 1, row1[j] + cost)));
row1.swap(row2);
}
return *std::min_element(row1.begin(), row1.end());
QPixmap
utils::scaleImageToPixmap(const QImage &img, int size)
{
// Deprecated in 5.13: const double sz =
// std::ceil(QApplication::desktop()->screen()->devicePixelRatioF() * (double)size);
const int sz = static_cast<int>(
std::ceil(QGuiApplication::primaryScreen()->devicePixelRatio() * (double)size));
return QPixmap::fromImage(img.scaled(sz, sz, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
QPixmap
utils::scaleDown(uint64_t maxWidth, uint64_t maxHeight, const QPixmap &source)
{
if (source.isNull())
return QPixmap();
const double widthRatio = (double)maxWidth / (double)source.width();
const double heightRatio = (double)maxHeight / (double)source.height();
const double minAspectRatio = std::min(widthRatio, heightRatio);
// Size of the output image.
int w, h = 0;
if (minAspectRatio > 1) {
w = source.width();
h = source.height();
} else {
w = static_cast<int>(static_cast<double>(source.width()) * minAspectRatio);
h = static_cast<int>(static_cast<double>(source.height()) * minAspectRatio);
return source.scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
QString
utils::mxcToHttp(const QUrl &url, const QString &server, int port)
{
auto mxcParts = mtx::client::utils::parse_mxc_url(url.toString().toStdString());
return QStringLiteral("https://%1:%2/_matrix/media/r0/download/%3/%4")
.arg(QString::fromStdString(mxcParts.server), QString::fromStdString(mxcParts.media_id));
QString
utils::humanReadableFingerprint(const std::string &ed25519)
{
return humanReadableFingerprint(QString::fromStdString(ed25519));
}
QString
utils::humanReadableFingerprint(const QString &ed25519)
{
QString fingerprint;
for (int i = 0; i < ed25519.length(); i = i + 4) {
fingerprint.append('\n');
else if (i < ed25519.length())
fingerprint.append(' ');
}
return fingerprint;
QString
utils::linkifyMessage(const QString &body)
{
// Convert to valid XML.
auto doc = body;
doc.replace(conf::strings::url_regex, conf::strings::url_html);
static QRegularExpression matrixURIRegex(
QStringLiteral("\\b(?<![\"'])(?>(matrix:[\\S]{5,}))(?![\"'])\\b"));
doc.replace(matrixURIRegex, conf::strings::url_html);
QString
utils::escapeBlacklistedHtml(const QString &rawStr)
Nicolas Werner
committed
static const std::set<QByteArray> allowedTags = {
"font", "/font", "del", "/del", "h1", "/h1", "h2", "/h2",
"h3", "/h3", "h4", "/h4", "h5", "/h5", "h6", "/h6",
"blockquote", "/blockquote", "p", "/p", "a", "/a", "ul", "/ul",
"ol", "/ol", "sup", "/sup", "sub", "/sub", "li", "/li",
"b", "/b", "i", "/i", "u", "/u", "strong", "/strong",
"em", "/em", "strike", "/strike", "code", "/code", "hr", "/hr",
"br", "br/", "div", "/div", "table", "/table", "thead", "/thead",
"tbody", "/tbody", "tr", "/tr", "th", "/th", "td", "/td",
"caption", "/caption", "pre", "/pre", "span", "/span", "img", "/img",
"details", "/details", "summary", "/summary"};
constexpr static const std::array tagNameEnds = {' ', '>'};
constexpr static const std::array attrNameEnds = {' ', '>', '=', '\t', '\r', '\n', '/', '\f'};
constexpr static const std::array attrValueEnds = {' ', '\t', '\r', '\n', '\f', '>'};
constexpr static const std::array spaceChars = {' ', '\t', '\r', '\n', '\f'};
Nicolas Werner
committed
QByteArray data = rawStr.toUtf8();
QByteArray buffer;
const int length = data.size();
buffer.reserve(length);
Nicolas Werner
committed
const auto end = data.cend();
for (auto pos = data.cbegin(); pos < end;) {
auto tagStart = std::find(pos, end, '<');
buffer.append(pos, static_cast<int>(tagStart - pos));
Nicolas Werner
committed
if (tagStart == end)
break;
const auto tagNameStart = tagStart + 1;
const auto tagNameEnd =
std::find_first_of(tagNameStart, end, tagNameEnds.begin(), tagNameEnds.end());
if (allowedTags.find(
QByteArray(tagNameStart, static_cast<int>(tagNameEnd - tagNameStart)).toLower()) ==
Nicolas Werner
committed
allowedTags.end()) {
// not allowed -> escape
buffer.append("<");
pos = tagNameStart;
continue;
} else {
buffer.append(tagStart, static_cast<int>(tagNameEnd - tagStart));
Nicolas Werner
committed
pos = tagNameEnd;
if (tagNameEnd != end) {
auto attrStart = tagNameEnd;
auto attrsEnd = std::find(attrStart, end, '>');
// we don't want to consume the slash of self closing tags as part of an attribute.
// However, obviously we don't want to move backwards, if there are no attributes.
if (*(attrsEnd - 1) == '/' && attrStart < attrsEnd)
Nicolas Werner
committed
attrsEnd -= 1;
pos = attrsEnd;
auto consumeSpaces = [attrsEnd](auto p) {
while (p < attrsEnd &&
std::find(spaceChars.begin(), spaceChars.end(), *p) != spaceChars.end())
p++;
return p;
};
attrStart = consumeSpaces(attrStart);
while (attrStart < attrsEnd) {
auto attrEnd = std::find_first_of(
attrStart, attrsEnd, attrNameEnds.begin(), attrNameEnds.end());
auto attrName =
QByteArray(attrStart, static_cast<int>(attrEnd - attrStart)).toLower();
Nicolas Werner
committed
auto sanitizeValue = [&attrName](QByteArray val) {
if (attrName == QByteArrayLiteral("src") && !val.startsWith("mxc://"))
return QByteArray();
else
return val;
};
attrStart = consumeSpaces(attrEnd);
if (attrName.isEmpty()) {
buffer.append(QUrl::toPercentEncoding(QString(QByteArray(attrStart, 1))));
attrStart++;
continue;
} else if (attrStart < attrsEnd) {
Nicolas Werner
committed
if (*attrStart == '=') {
attrStart = consumeSpaces(attrStart + 1);
if (attrStart < attrsEnd) {
// we fall through here if the value is empty to transform attr=""
// into attr, because otherwise we can't style it
if (*attrStart == '"') {
attrStart += 1;
auto valueEnd = std::find(attrStart, attrsEnd, '"');
if (valueEnd == attrsEnd)
break;
auto val = sanitizeValue(QByteArray(
attrStart, static_cast<int>(valueEnd - attrStart)));
Nicolas Werner
committed
attrStart = consumeSpaces(valueEnd + 1);
if (!val.isEmpty()) {
buffer.append(' ');
buffer.append(attrName);
buffer.append("=\"");
buffer.append(val);
buffer.append('"');
continue;
}
} else if (*attrStart == '\'') {
attrStart += 1;
auto valueEnd = std::find(attrStart, attrsEnd, '\'');
if (valueEnd == attrsEnd)
break;
auto val = sanitizeValue(QByteArray(
attrStart, static_cast<int>(valueEnd - attrStart)));
Nicolas Werner
committed
attrStart = consumeSpaces(valueEnd + 1);
if (!val.isEmpty()) {
buffer.append(' ');
buffer.append(attrName);
buffer.append("=\'");
buffer.append(val);
buffer.append('\'');
continue;
}
} else {
auto valueEnd = std::find_first_of(attrStart,
attrsEnd,
attrValueEnds.begin(),
attrValueEnds.end());
auto val = sanitizeValue(QByteArray(
attrStart, static_cast<int>(valueEnd - attrStart)));
if (val.contains('"'))
continue;
Nicolas Werner
committed
buffer.append(' ');
buffer.append(attrName);
buffer.append("=\"");
buffer.append(val);
buffer.append('"');
Nicolas Werner
committed
continue;
}
}
}
}
buffer.append(' ');
buffer.append(attrName);
Nicolas Werner
committed
static void
rainbowify(cmark_node *node)
// create iterator over node
cmark_iter *iter = cmark_iter_new(node);
// First loop to get total text length
int textLen = 0;
while (cmark_iter_next(iter) != CMARK_EVENT_DONE) {
cmark_node *cur = cmark_iter_get_node(iter);
// only text nodes (no code or semilar)
if (cmark_node_get_type(cur) != CMARK_NODE_TEXT)
continue;
// count up by length of current node's text
QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme,
QString(cmark_node_get_literal(cur)));
while (tbf.toNextBoundary() != -1)
textLen++;
}
// create new iter to start over
cmark_iter_free(iter);
iter = cmark_iter_new(node);
// Second loop to rainbowify
int charIdx = 0;
while (cmark_iter_next(iter) != CMARK_EVENT_DONE) {
cmark_node *cur = cmark_iter_get_node(iter);
// only text nodes (no code or similar)
if (cmark_node_get_type(cur) != CMARK_NODE_TEXT)
continue;
// get text in current node
QString nodeText(cmark_node_get_literal(cur));
// create buffer to append rainbow text to
QString buf;
int boundaryStart = 0;
int boundaryEnd = 0;
// use QTextBoundaryFinder to iterate over graphemes
QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme, nodeText);
while ((boundaryEnd = tbf.toNextBoundary()) != -1) {
charIdx++;
// Split text to get current char
auto curChar = QStringView(nodeText).mid(boundaryStart, boundaryEnd - boundaryStart);
boundaryStart = boundaryEnd;
// Don't rainbowify whitespaces
if (curChar.trimmed().isEmpty() || utils::codepointIsEmoji(curChar.toUcs4().at(0))) {
buf.append(curChar);
}
// get correct color for char index
// Use colors as described here:
// https://shark.comfsm.fm/~dleeling/cis/hsl_rainbow.html
auto color = QColor::fromHslF((charIdx - 1.0) / textLen * (5. / 6.), 0.9, 0.5);
// format color for HTML
auto colorString = color.name(QColor::NameFormat::HexRgb);
// create HTML element for current char
auto curCharColored =
QStringLiteral("<font color=\"%0\">%1</font>").arg(colorString).arg(curChar);
// append colored HTML element to buffer
buf.append(curCharColored);
// create HTML_INLINE node to prevent HTML from being escaped
auto htmlNode = cmark_node_new(CMARK_NODE_HTML_INLINE);
// set content of HTML node to buffer contents
cmark_node_set_literal(htmlNode, buf.toUtf8().data());
// replace current node with HTML node
cmark_node_replace(cur, htmlNode);
// free memory of old node
cmark_node_free(cur);
}
cmark_iter_free(iter);
}
static std::string
extract_spoiler_warning(std::string &inside_spoiler)
{
std::string spoiler_text;
if (auto spoilerTextEnd = inside_spoiler.find("|"); spoilerTextEnd != std::string::npos) {
spoiler_text = inside_spoiler.substr(0, spoilerTextEnd);
inside_spoiler = inside_spoiler.substr(spoilerTextEnd + 1);
}
return QString::fromStdString(spoiler_text).replace('"', """).toStdString();
}
// TODO(Nico): Add tests :D
static void
process_spoilers(cmark_node *node)
{
auto iter = cmark_iter_new(node);
while (cmark_iter_next(iter) != CMARK_EVENT_DONE) {
cmark_node *cur = cmark_iter_get_node(iter);
// only text nodes (no code or similar)
if (cmark_node_get_type(cur) != CMARK_NODE_TEXT) {
continue;
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
std::string_view content = cmark_node_get_literal(cur);
if (auto posStart = content.find("||"); posStart != std::string::npos) {
// we have the start of the spoiler
if (auto posEnd = content.find("||", posStart + 2); posEnd != std::string::npos) {
// we have the end of the spoiler in the same node
std::string before_spoiler = std::string(content.substr(0, posStart));
std::string inside_spoiler =
std::string(content.substr(posStart + 2, posEnd - 2 - posStart));
std::string after_spoiler = std::string(content.substr(posEnd + 2));
std::string spoiler_text = extract_spoiler_warning(inside_spoiler);
// create the new nodes
auto before_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
cmark_node_set_literal(before_node, before_spoiler.c_str());
auto after_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
cmark_node_set_literal(after_node, after_spoiler.c_str());
auto block = cmark_node_new(cmark_node_type::CMARK_NODE_CUSTOM_INLINE);
cmark_node_set_on_enter(
block, ("<span data-mx-spoiler=\"" + spoiler_text + "\">").c_str());
cmark_node_set_on_exit(block, "</span>");
auto child_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
cmark_node_set_literal(child_node, inside_spoiler.c_str());
cmark_node_append_child(block, child_node);
// insert the new nodes into the tree
cmark_node_replace(cur, block);
cmark_node_insert_before(block, before_node);
cmark_node_insert_after(block, after_node);
// cleanup the replaced node
cmark_node_free(cur);
// fixup the iterator
cmark_iter_reset(iter, block, CMARK_EVENT_EXIT);
} else {
// no end found, but lets try sibling nodes
for (auto next = cmark_node_next(cur); next != nullptr;
next = cmark_node_next(next)) {
// only text nodes again
if (cmark_node_get_type(next) != CMARK_NODE_TEXT)
continue;
std::string_view next_content = cmark_node_get_literal(next);
Nicolas Werner
committed
if (auto posEndNext = next_content.find("||");
posEndNext != std::string_view::npos) {
// We found the end of the spoiler
std::string before_spoiler = std::string(content.substr(0, posStart));
Nicolas Werner
committed
std::string after_spoiler =
std::string(next_content.substr(posEndNext + 2));
std::string inside_spoiler_start =
std::string(content.substr(posStart + 2));
std::string inside_spoiler_end =
Nicolas Werner
committed
std::string(next_content.substr(0, posEndNext));
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
std::string spoiler_text = extract_spoiler_warning(inside_spoiler_start);
// save all the nodes inside the spoiler for later
std::vector<cmark_node *> child_nodes;
for (auto kid = cmark_node_next(cur); kid != nullptr && kid != next;
kid = cmark_node_next(kid)) {
child_nodes.push_back(kid);
}
// create the new nodes
auto before_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
cmark_node_set_literal(before_node, before_spoiler.c_str());
auto after_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
cmark_node_set_literal(after_node, after_spoiler.c_str());
auto block = cmark_node_new(cmark_node_type::CMARK_NODE_CUSTOM_INLINE);
cmark_node_set_on_enter(
block, ("<span data-mx-spoiler=\"" + spoiler_text + "\">").c_str());
cmark_node_set_on_exit(block, "</span>");
// create the content inside the spoiler by adding the old text at the start
// and the end as well as all the existing children
auto child_node_start = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
cmark_node_set_literal(child_node_start, inside_spoiler_start.c_str());
cmark_node_append_child(block, child_node_start);
for (auto &child : child_nodes)
cmark_node_append_child(block, child);
auto child_node_end = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
cmark_node_set_literal(child_node_end, inside_spoiler_end.c_str());
cmark_node_append_child(block, child_node_end);
// insert the new nodes into the tree
cmark_node_replace(cur, block);
cmark_node_insert_before(block, before_node);
cmark_node_insert_after(block, after_node);
// cleanup removed nodes
cmark_node_free(cur);
cmark_node_free(next);
// fixup the iterator
cmark_iter_reset(iter, block, CMARK_EVENT_EXIT);
break;
}
}
}
}
}
cmark_iter_free(iter);
}
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
static void
process_strikethrough(cmark_node *node)
{
auto iter = cmark_iter_new(node);
while (cmark_iter_next(iter) != CMARK_EVENT_DONE) {
cmark_node *cur = cmark_iter_get_node(iter);
// only text nodes (no code or similar)
if (cmark_node_get_type(cur) != CMARK_NODE_TEXT) {
continue;
}
std::string_view content = cmark_node_get_literal(cur);
if (auto posStart = content.find("~~"); posStart != std::string::npos) {
// we have the start of the strikethrough
if (auto posEnd = content.find("~~", posStart + 2); posEnd != std::string::npos) {
// we have the end of the strikethrough in the same node
std::string before_strike = std::string(content.substr(0, posStart));
std::string inside_strike =
std::string(content.substr(posStart + 2, posEnd - 2 - posStart));
std::string after_strike = std::string(content.substr(posEnd + 2));
// create the new nodes
auto before_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
cmark_node_set_literal(before_node, before_strike.c_str());
auto after_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
cmark_node_set_literal(after_node, after_strike.c_str());
auto block = cmark_node_new(cmark_node_type::CMARK_NODE_CUSTOM_INLINE);
cmark_node_set_on_enter(block, "<del>");
cmark_node_set_on_exit(block, "</del>");
auto child_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
cmark_node_set_literal(child_node, inside_strike.c_str());
cmark_node_append_child(block, child_node);
// insert the new nodes into the tree
cmark_node_replace(cur, block);
cmark_node_insert_before(block, before_node);
cmark_node_insert_after(block, after_node);
// cleanup the replaced node
cmark_node_free(cur);
// fixup the iterator
cmark_iter_reset(iter, block, CMARK_EVENT_EXIT);
} else {
// no end found, but lets try sibling nodes
for (auto next = cmark_node_next(cur); next != nullptr;
next = cmark_node_next(next)) {
// only text nodes again
if (cmark_node_get_type(next) != CMARK_NODE_TEXT)
continue;
std::string_view next_content = cmark_node_get_literal(next);
if (auto posEndNext = next_content.find("~~");
posEndNext != std::string_view::npos) {
// We found the end of the strikethrough
std::string before_strike = std::string(content.substr(0, posStart));
std::string after_strike = std::string(next_content.substr(posEndNext + 2));
std::string inside_strike_start = std::string(content.substr(posStart + 2));
std::string inside_strike_end =
std::string(next_content.substr(0, posEndNext));
// save all the nodes inside the strikethrough for later
std::vector<cmark_node *> child_nodes;
for (auto kid = cmark_node_next(cur); kid != nullptr && kid != next;
kid = cmark_node_next(kid)) {
child_nodes.push_back(kid);
}
// create the new nodes
auto before_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
cmark_node_set_literal(before_node, before_strike.c_str());
auto after_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
cmark_node_set_literal(after_node, after_strike.c_str());
auto block = cmark_node_new(cmark_node_type::CMARK_NODE_CUSTOM_INLINE);
cmark_node_set_on_enter(block, "<del>");
cmark_node_set_on_exit(block, "</del>");
// create the content inside the strikethrough by adding the old text at the
// start and the end as well as all the existing children
auto child_node_start = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
cmark_node_set_literal(child_node_start, inside_strike_start.c_str());
cmark_node_append_child(block, child_node_start);
for (auto &child : child_nodes)
cmark_node_append_child(block, child);
auto child_node_end = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
cmark_node_set_literal(child_node_end, inside_strike_end.c_str());
cmark_node_append_child(block, child_node_end);
// insert the new nodes into the tree
cmark_node_replace(cur, block);
cmark_node_insert_before(block, before_node);
cmark_node_insert_after(block, after_node);
// cleanup removed nodes
cmark_node_free(cur);
cmark_node_free(next);
// fixup the iterator
cmark_iter_reset(iter, block, CMARK_EVENT_EXIT);
break;
}
}
}
}
}
cmark_iter_free(iter);
}
utils::markdownToHtml(const QString &text, bool rainbowify_, bool noExtensions)
{
const auto str = text.toUtf8();
cmark_node *const node = cmark_parse_document(str.constData(), str.size(), CMARK_OPT_UNSAFE);
if (!noExtensions) {
process_strikethrough(node);
process_spoilers(node);
if (rainbowify_) {
rainbowify(node);
}
const char *tmp_buf = cmark_render_html(
node,
// by default make single linebreaks <br> tags
noExtensions ? CMARK_OPT_UNSAFE : (CMARK_OPT_UNSAFE | CMARK_OPT_HARDBREAKS));
// Copy the null terminated output buffer.
std::string html(tmp_buf);
// The buffer is no longer needed.
free((char *)tmp_buf);
cmark_node_free(node);
auto result = escapeBlacklistedHtml(QString::fromStdString(html)).trimmed();
if (!noExtensions) {
result = linkifyMessage(std::move(result)).trimmed();
}
if (result.count(QStringLiteral("<p>")) == 1 && result.startsWith(QLatin1String("<p>")) &&
result.endsWith(QLatin1String("</p>"))) {
result = result.mid(3, result.size() - 3 - 4);
}
utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html)
auto getFormattedBody = [related]() -> QString {
using MsgType = mtx::events::MessageType;
switch (related.type) {
case MsgType::File: {
return QStringLiteral("sent a file.");
}
case MsgType::Image: {
return QStringLiteral("sent an image.");
}
case MsgType::Audio: {
return QStringLiteral("sent an audio file.");
}
case MsgType::Video: {
return QStringLiteral("sent a video");
};
return QString("<mx-reply><blockquote><a "
"href=\"https://matrix.to/#/%1/%2\">In reply "
"to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br"
"/>%5</blockquote></mx-reply>")
.arg(related.room,
QString::fromStdString(related.related_event),
related.quoted_user,
related.quoted_user,
getFormattedBody()) +
html;
}
QString
utils::getQuoteBody(const RelatedInfo &related)
{
using MsgType = mtx::events::MessageType;
switch (related.type) {
case MsgType::File: {
return QStringLiteral("sent a file.");
return QStringLiteral("sent an image.");