// SPDX-FileCopyrightText: 2023 James Graham "_L1) == 1 && outputString.count(" "_L1) && outputString.endsWith(" "_L1);
outputString.remove("
"_s);
}
// Linkify any plain text urls
m_dataBuffer = linkifyUrls(m_dataBuffer);
// Apply user style
m_dataBuffer.replace(TextRegex::userPill, uR"(\1)"_s);
// Make all media URLs resolvable.
if (room && event) {
QRegularExpressionMatchIterator i = TextRegex::mxcImage.globalMatch(m_dataBuffer);
while (i.hasNext()) {
const QRegularExpressionMatch match = i.next();
const QUrl mediaUrl = room->makeMediaUrl(event->id(), QUrl(u"mxc://"_s + match.captured(2) + u'/' + match.captured(3)));
QStringList extraAttributes = match.captured(4).split(QChar::Space);
const bool isEmoticon = match.captured(1).contains(u"data-mx-emoticon"_s);
// If the image does not have an explicit width, but has a vertical-align it's most likely an emoticon.
// We must do some pre-processing for it to show up nicely in and around text.
if (isEmoticon) {
// Align it properly
extraAttributes.append(u"style=\"%1\""_s.arg(customEmojiStyle));
}
m_dataBuffer.replace(match.captured(0),
u"');
}
}
// Strip any disallowed tags/attributes.
QString outputString;
m_nextTokenType = nextTokenType(m_dataBuffer, m_pos, m_nextToken, m_nextTokenType);
while (m_pos < m_dataBuffer.length()) {
next();
QString nextTokenBuffer = m_nextToken;
if (m_nextTokenType == Type::Text || m_nextTokenType == Type::TextCode) {
nextTokenBuffer = escapeHtml(nextTokenBuffer);
} else if (m_nextTokenType == Type::Tag) {
if (!isAllowedTag(getTagType(m_nextToken))) {
nextTokenBuffer = QString();
} else if ((getTagType(m_nextToken) == u"br"_s && stripNewlines)) {
nextTokenBuffer = u' ';
}
nextTokenBuffer = cleanAttributes(getTagType(m_nextToken), nextTokenBuffer);
}
outputString.append(nextTokenBuffer);
m_nextTokenType = nextTokenType(m_dataBuffer, m_pos, m_nextToken, m_nextTokenType);
}
if (isEdited) {
if (outputString.endsWith(u"
%1
"_s.arg(editString())); } else { outputString.append(editString()); } } /** * Replace"_L1) == 1 && outputString.count("
"_L1) == 1 && outputString.startsWith(""_L1) && outputString.endsWith("
"_L1)) { outputString.remove(""_L1); outputString.remove("
"_L1); } return outputString; } QString TextHandler::handleRecievePlainText(Qt::TextFormat inputFormat, const bool &stripNewlines) { m_pos = 0; m_dataBuffer = m_data; // Strip mx-reply if present. m_dataBuffer.remove(TextRegex::removeRichReply); // Escaping then unescaping allows < and > to be maintained in a plain text string // otherwise markdownToHTML will strip what it thinks is a bad html tag entirely. if (inputFormat == Qt::PlainText) { m_dataBuffer = escapeHtml(m_dataBuffer); } /** * This seems counterproductive but by converting any markup which could * arrive (e.g. in a caption body) it can then be stripped by the same code. */ m_dataBuffer = markdownToHTML(m_dataBuffer); // This is how \n is converted and for plain text we need it to just be tag.
if (nextTokenType == Text) {
int pos = 0;
while (pos < string.size()) {
pos = string.indexOf(u'<', pos);
if (pos == -1) {
pos = string.size();
} else {
const auto tagType = getTagType(string.mid(pos, string.indexOf(u'>', pos) - pos));
if (blockTags.contains(tagType)) {
return pos;
}
}
pos++;
}
return string.size();
}
int tagEndPos = string.indexOf(u'>');
QString tag = string.first(tagEndPos + 1);
QString tagType = getTagType(tag);
// If the start tag is not a block tag there can be only 1 block.
if (!blockTags.contains(tagType)) {
return string.size();
}
const auto closeTag = u"%1>"_s.arg(tagType);
int closeTagPos = string.indexOf(closeTag);
// If the close tag can't be found assume malformed html and process as single block.
if (closeTagPos == -1) {
return string.size();
}
return closeTagPos + closeTag.size();
}
MessageComponent TextHandler::nextBlock(const QString &string,
int nextBlockPos,
Qt::TextFormat inputFormat,
const NeoChatRoom *room,
const Quotient::RoomEvent *event,
bool isEdited)
{
if (string.isEmpty()) {
return {};
}
int tagEndPos = string.indexOf(u'>');
QString tag = string.first(tagEndPos + 1);
QString tagType = getTagType(tag);
const auto messageComponentType = MessageComponentType::typeForTag(tagType);
QVariantMap attributes;
if (messageComponentType == MessageComponentType::Code) {
attributes = getAttributes(u"code"_s, string.mid(tagEndPos + 1, string.indexOf(u'>', tagEndPos + 1) - tagEndPos));
}
auto content = stripBlockTags(string.first(nextBlockPos), tagType);
setData(content);
switch (messageComponentType) {
case MessageComponentType::Code:
content = unescapeHtml(content);
break;
default:
content = handleRecieveRichText(inputFormat, room, event, false, isEdited);
}
return MessageComponent{messageComponentType, content, attributes};
}
QString TextHandler::stripBlockTags(QString string, const QString &tagType) const
{
if (blockTags.contains(tagType) && tagType != u"ol"_s && tagType != u"ul"_s && tagType != u"table"_s && string.startsWith(u"<%1"_s.arg(tagType))) {
string.remove(0, string.indexOf(u'>') + 1).remove(string.indexOf(u"%1>"_s.arg(tagType)), string.size());
}
if (string.startsWith(u"\n"_s)) {
string.remove(0, 1);
}
if (string.endsWith(u"\n"_s)) {
string.remove(string.size() - 1, string.size());
}
if (tagType == u"pre"_s) {
if (string.startsWith(u"') + 1);
string.remove(string.indexOf(u""_s), string.size());
}
if (string.endsWith(u"\n"_s)) {
string.remove(string.size() - 1, string.size());
}
}
if (tagType == u"blockquote"_s) {
if (string.startsWith(u"
"_s)) { string.remove(0, string.indexOf(u'>') + 1); string.remove(string.indexOf(u"
"_s), string.size()); } // This is not a normal quotation mark but U+201C if (!string.startsWith(u'“')) { string.prepend(u'“'); } // This is U+201D if (!string.endsWith(u'”')) { string.append(u'”'); } } return string; } QString TextHandler::getTagType(const QString &tagToken) const { if (tagToken.isEmpty() || tagToken.length() < 2) { return QString(); } const int tagTypeStart = tagToken[1] == u'/' ? 2 : 1; const int tagTypeEnd = tagToken.indexOf(TextRegex::endTagType, tagTypeStart); return tagToken.mid(tagTypeStart, tagTypeEnd - tagTypeStart); } bool TextHandler::isCloseTag(const QString &tagToken) const { if (tagToken.isEmpty()) { return false; } return tagToken[1] == u'/'; } QString TextHandler::getAttributeType(const QString &string) { if (!string.contains(u'=')) { return string; } const int equalsPos = string.indexOf(u'='); return string.left(equalsPos); } QString TextHandler::getAttributeData(const QString &string, bool stripQuotes) { if (!string.contains(u'=')) { return QString(); } const int equalsPos = string.indexOf(u'='); auto data = string.right(string.length() - equalsPos - 1); if (stripQuotes) { data = TextRegex::attributeData.match(data).captured(1); } return data; } bool TextHandler::isAllowedTag(const QString &type) { return allowedTags.contains(type); } bool TextHandler::isAllowedAttribute(const QString &tag, const QString &attribute) { return allowedAttributes[tag].contains(attribute); } bool TextHandler::isAllowedLink(const QString &link, bool isImg) { const QUrl linkUrl = QUrl(link); if (isImg) { return !linkUrl.isRelative() && linkUrl.scheme() == u"mxc"_s; } else { return !linkUrl.isRelative() && allowedLinkSchemes.contains(linkUrl.scheme()); } } QString TextHandler::cleanAttributes(const QString &tag, const QString &tagString) { int nextAttributeIndex = tagString.indexOf(u' ', 1); if (nextAttributeIndex != -1) { QString outputString = tagString.left(nextAttributeIndex); QString nextAttribute; int nextSpaceIndex; nextAttributeIndex += 1; while (nextAttributeIndex < tagString.length()) { nextSpaceIndex = tagString.indexOf(TextRegex::endAttributeType, nextAttributeIndex); if (nextSpaceIndex == -1) { nextSpaceIndex = tagString.length(); } nextAttribute = tagString.mid(nextAttributeIndex, nextSpaceIndex - nextAttributeIndex); if (isAllowedAttribute(tag, getAttributeType(nextAttribute))) { QString style; if (tag == u"img"_s && getAttributeType(nextAttribute) == u"src"_s) { QString attributeData = TextRegex::attributeData.match(getAttributeData(nextAttribute)).captured(1); if (isAllowedLink(attributeData, true)) { outputString.append(u' ' + nextAttribute); } } else if (tag == u'a' && getAttributeType(nextAttribute) == u"href"_s) { QString attributeData = TextRegex::attributeData.match(getAttributeData(nextAttribute)).captured(1); if (isAllowedLink(attributeData)) { outputString.append(u' ' + nextAttribute); } } else if (tag == u"code"_s && getAttributeType(nextAttribute) == u"class"_s) { if (getAttributeData(nextAttribute).remove(u'"').startsWith(u"language-"_s)) { outputString.append(u' ' + nextAttribute); } } else if (tag == u"img"_s && getAttributeType(nextAttribute) == u"style"_s) { const QString attributeData = TextRegex::attributeData.match(getAttributeData(nextAttribute)).captured(1); // Ignore every other style attribute except for our own, which we use to align custom emoticons if (attributeData == customEmojiStyle) { outputString.append(u' ' + nextAttribute); } } else if (getAttributeType(nextAttribute) == u"data-mx-color"_s) { const QString attributeData = TextRegex::attributeData.match(getAttributeData(nextAttribute)).captured(1); style.append(u"color: "_s + attributeData + u';'); } else if (getAttributeType(nextAttribute) == u"data-mx-bg-color"_s) { const QString attributeData = TextRegex::attributeData.match(getAttributeData(nextAttribute)).captured(1); style.append(u"background-color: "_s + attributeData + u';'); } else { outputString.append(u' ' + nextAttribute); } if (!style.isEmpty()) { outputString.append(u" style=\""_s + style + u'"'); } } nextAttributeIndex = nextSpaceIndex + 1; } outputString += u'>'; return outputString; } return tagString; } QVariantMap TextHandler::getAttributes(const QString &tag, const QString &tagString) { QVariantMap attributes; int nextAttributeIndex = tagString.indexOf(u' ', 1); if (nextAttributeIndex != -1) { QString nextAttribute; int nextSpaceIndex; nextAttributeIndex += 1; while (nextAttributeIndex < tagString.length()) { nextSpaceIndex = tagString.indexOf(TextRegex::endAttributeType, nextAttributeIndex); if (nextSpaceIndex == -1) { nextSpaceIndex = tagString.length(); } nextAttribute = tagString.mid(nextAttributeIndex, nextSpaceIndex - nextAttributeIndex); if (isAllowedAttribute(tag, getAttributeType(nextAttribute))) { if (tag == u"img"_s && getAttributeType(nextAttribute) == u"src"_s) { QString attributeData = TextRegex::attributeData.match(getAttributeData(nextAttribute)).captured(1); if (isAllowedLink(attributeData, true)) { attributes[getAttributeType(nextAttribute)] = getAttributeData(nextAttribute, true); } } else if (tag == u'a' && getAttributeType(nextAttribute) == u"href"_s) { QString attributeData = TextRegex::attributeData.match(getAttributeData(nextAttribute)).captured(1); if (isAllowedLink(attributeData)) { attributes[getAttributeType(nextAttribute)] = getAttributeData(nextAttribute, true); } } else if (tag == u"code"_s && getAttributeType(nextAttribute) == u"class"_s) { if (getAttributeData(nextAttribute).remove(u'"').startsWith(u"language-"_s)) { attributes[getAttributeType(nextAttribute)] = convertCodeLanguageString(getAttributeData(nextAttribute, true)); } } else { attributes[getAttributeType(nextAttribute)] = getAttributeData(nextAttribute, true); } } nextAttributeIndex = nextSpaceIndex + 1; } } return attributes; } QList"_s) == stringIn.left(index).count(u""_s)) {
auto replacement = u"%1"_s.arg(match.captured(1));
stringIn = stringIn.replace(index, match.captured(0).size(), replacement);
} else {
skip = match.captured().length();
}
}
start = index + skip;
match = {};
}
start = 0;
match = {};
for (int index = 0; index != -1; index = stringIn.indexOf(TextRegex::plainUrl, start, &match)) {
int skip = 0;
if (match.captured(0).size() > 0) {
if (stringIn.left(index).count(u""_s) == stringIn.left(index).count(u""_s)) {
auto replacement = u"%1"_s.arg(match.captured(1));
stringIn = stringIn.replace(index, match.captured(0).size(), replacement);
skip = replacement.length();
} else {
skip = match.captured().length();
}
}
start = index + skip;
match = {};
}
start = 0;
match = {};
for (int index = 0; index != -1; index = stringIn.indexOf(TextRegex::emailAddress, start, &match)) {
int skip = 0;
if (match.captured(0).size() > 0) {
if (stringIn.left(index).count(u""_s) == stringIn.left(index).count(u""_s)) {
auto replacement = u"%1"_s.arg(match.captured(2));
stringIn = stringIn.replace(index, match.captured(0).size(), replacement);
skip = replacement.length();
} else {
skip = match.captured().length();
}
}
start = index + skip;
match = {};
}
return stringIn;
}
QString TextHandler::editString() const
{
Kirigami::Platform::PlatformTheme *theme =
static_cast