ZaStoGram_desktop/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_serialize.cpp
John Preston 9e4976b2af Derive the rich-button type bound from Type::kCount
Task: 2026/08/14/derive-button-type-bound-from-enum-count
2026-08-14 17:56:06 +04:00

508 lines
15 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "iv/markdown/iv_markdown_prepare_serialize.h"
#include "base/variant.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include "styles/style_iv.h"
#include <QtCore/QByteArray>
#include <limits>
#include <utility>
namespace Iv::Markdown {
namespace {
[[nodiscard]] QString EncodeInlineTextObjectBytes(const QByteArray &value) {
return QString::fromUtf8(value.toPercentEncoding());
}
[[nodiscard]] QByteArray DecodeInlineTextObjectBytes(QStringView value) {
return QByteArray::fromPercentEncoding(value.toLatin1());
}
[[nodiscard]] QString EncodeInlineTextObjectField(const QString &value) {
return EncodeInlineTextObjectBytes(value.toUtf8());
}
[[nodiscard]] QString DecodeInlineTextObjectField(QStringView value) {
return QString::fromUtf8(DecodeInlineTextObjectBytes(value));
}
[[nodiscard]] QString RichButtonLabelEntityName(EntityType type) {
return (type == EntityType::CustomEmoji)
? u"custom-emoji"_q
: (type == EntityType::FormattedDate)
? u"formatted-date"_q
: QString();
}
[[nodiscard]] EntityType RichButtonLabelEntityType(QStringView name) {
return (name == u"custom-emoji"_q)
? EntityType::CustomEmoji
: (name == u"formatted-date"_q)
? EntityType::FormattedDate
: EntityType::Invalid;
}
[[nodiscard]] QString SerializeRichButtonLabel(const TextWithEntities &text) {
auto result = EncodeInlineTextObjectField(text.text);
for (const auto &entity : text.entities) {
const auto name = RichButtonLabelEntityName(entity.type());
if (name.isEmpty()) {
continue;
}
result += u"|"_q
+ name
+ u","_q
+ QString::number(entity.offset())
+ u","_q
+ QString::number(entity.length())
+ u","_q
+ EncodeInlineTextObjectField(entity.data());
}
return result;
}
[[nodiscard]] TextWithEntities ParseRichButtonLabel(QStringView data) {
const auto parts = data.split(QChar('|'), Qt::KeepEmptyParts);
if (parts.empty()) {
return {};
}
auto result = TextWithEntities{
DecodeInlineTextObjectField(parts[0]),
};
const auto size = int(result.text.size());
for (auto i = 1, count = int(parts.size()); i != count; ++i) {
const auto fields = parts[i].split(QChar(','), Qt::KeepEmptyParts);
if (fields.size() != 4) {
continue;
}
auto offsetOk = false;
auto lengthOk = false;
const auto type = RichButtonLabelEntityType(fields[0]);
const auto offset = fields[1].toInt(&offsetOk);
const auto length = fields[2].toInt(&lengthOk);
if (type == EntityType::Invalid
|| !offsetOk
|| !lengthOk
|| offset < 0
|| length <= 0
|| offset + length > size) {
continue;
}
result.entities.push_back(EntityInText(
type,
offset,
length,
DecodeInlineTextObjectField(fields[3])));
}
return result;
}
[[nodiscard]] std::optional<InlineTextObjectButtonData> InlineLinkButtonFor(
const EntityInText &entity) {
if (entity.type() != EntityType::CustomEmoji) {
return std::nullopt;
}
const auto parsed = ParseInlineTextObjectEntity(entity.data());
if (!parsed) {
return std::nullopt;
}
const auto button = std::get_if<InlineTextObjectButtonData>(&parsed->data);
return (button && button->link)
? std::make_optional(*button)
: std::nullopt;
}
void ReplaceInlineObjectText(
TextWithEntities *text,
const EntityInText &object,
const QString &replacement) {
const auto offset = object.offset();
const auto delta = int(replacement.size()) - object.length();
text->text.replace(offset, object.length(), replacement);
for (auto &entity : text->entities) {
if (&entity == &object) {
continue;
} else if (entity.offset() > offset) {
entity.shiftRight(delta);
} else if (entity.offset() + entity.length() > offset) {
entity.shrinkFromRight(-delta);
}
}
}
[[nodiscard]] EntitiesInText::iterator ExpandInlineObjectEntities(
TextWithEntities *text,
EntitiesInText::iterator i,
const EntitiesInText &nested) {
auto &entities = text->entities;
const auto offset = i->offset();
auto at = int(i - entities.begin());
entities.erase(i);
for (const auto &entity : nested) {
entities.insert(at++, EntityInText(
entity.type(),
entity.offset() + offset,
entity.length(),
entity.data()));
}
return entities.begin() + at;
}
[[nodiscard]] int TextSizeForFormula(const style::TextStyle &textStyle) {
return std::max(textStyle.font->height, 1);
}
} // namespace
QString SerializeInlineTextObjectEntity(const InlineTextObjectEntity &object) {
switch (object.kind) {
case InlineTextObjectKind::Formula: {
const auto data = std::get_if<InlineTextObjectFormulaData>(&object.data);
if (!data) {
return QString();
}
return u"iv-markdown:inline-text-object;formula;"_q
+ EncodeInlineTextObjectField(data->copySource)
+ u";"_q
+ EncodeInlineTextObjectField(data->trimmedTex);
} break;
case InlineTextObjectKind::IvImage: {
const auto data = std::get_if<InlineTextObjectIvImageData>(&object.data);
if (!data) {
return QString();
}
return u"iv-markdown:inline-text-object;iv-image;"_q
+ QString::number(data->documentId)
+ u";"_q
+ QString::number(data->width)
+ u";"_q
+ QString::number(data->height)
+ u";"_q
+ EncodeInlineTextObjectField(data->replacementText);
} break;
case InlineTextObjectKind::Button: {
const auto data = std::get_if<InlineTextObjectButtonData>(&object.data);
if (!data) {
return QString();
}
const auto label = SerializeRichButtonLabel(data->label);
return u"iv-markdown:inline-text-object;button;"_q
+ EncodeInlineTextObjectField(label)
+ u";"_q
+ QString::number(int(data->color))
+ u";"_q
+ QString::number(int(data->type))
+ u";"_q
+ QString::number(data->link ? 1 : 0)
+ u";"_q
+ EncodeInlineTextObjectBytes(data->data)
+ u";"_q
+ QString::number(data->buttonId)
+ u";"_q
+ QString::number(data->peerTypes.value());
} break;
}
return QString();
}
std::optional<InlineTextObjectEntity> ParseInlineTextObjectEntity(
QStringView data) {
const auto parts = data.split(QChar(';'), Qt::KeepEmptyParts);
if (parts.size() < 2
|| parts[0] != u"iv-markdown:inline-text-object"_q) {
return std::nullopt;
}
if (parts[1] == u"formula"_q) {
if (parts.size() != 4) {
return std::nullopt;
}
return InlineTextObjectEntity{
.kind = InlineTextObjectKind::Formula,
.data = InlineTextObjectFormulaData{
.copySource = DecodeInlineTextObjectField(parts[2]),
.trimmedTex = DecodeInlineTextObjectField(parts[3]),
},
};
} else if (parts[1] == u"iv-image"_q) {
if (parts.size() != 6) {
return std::nullopt;
}
auto documentIdOk = false;
auto widthOk = false;
auto heightOk = false;
const auto documentId = parts[2].toULongLong(&documentIdOk);
const auto width = parts[3].toInt(&widthOk);
const auto height = parts[4].toInt(&heightOk);
if (!documentIdOk || !widthOk || !heightOk) {
return std::nullopt;
}
return InlineTextObjectEntity{
.kind = InlineTextObjectKind::IvImage,
.data = InlineTextObjectIvImageData{
.documentId = documentId,
.width = width,
.height = height,
.replacementText = DecodeInlineTextObjectField(parts[5]),
},
};
} else if (parts[1] == u"button"_q) {
if (parts.size() != 9) {
return std::nullopt;
}
using Color = HistoryMessageMarkupButton::Color;
using Type = HistoryMessageMarkupButton::Type;
using PeerTypes = InlineBots::PeerTypes;
auto colorOk = false;
auto typeOk = false;
auto buttonIdOk = false;
auto peerTypesOk = false;
const auto color = parts[3].toInt(&colorOk);
const auto type = parts[4].toInt(&typeOk);
const auto buttonId = parts[7].toLongLong(&buttonIdOk);
const auto peerTypes = parts[8].toUInt(&peerTypesOk);
if (!colorOk
|| color < 0
|| color > int(Color::Success)
|| !typeOk
|| type < 0
|| type >= int(Type::kCount)
|| !buttonIdOk
|| !peerTypesOk
|| peerTypes > uint(
std::numeric_limits<PeerTypes::Type>::max())) {
return std::nullopt;
}
const auto label = DecodeInlineTextObjectField(parts[2]);
return InlineTextObjectEntity{
.kind = InlineTextObjectKind::Button,
.data = InlineTextObjectButtonData{
.label = ParseRichButtonLabel(label),
.data = DecodeInlineTextObjectBytes(parts[6]),
.buttonId = buttonId,
.type = Type(type),
.color = Color(color),
.peerTypes = PeerTypes::from_raw(PeerTypes::Type(peerTypes)),
.link = (parts[5] == u"1"_q),
},
};
}
return std::nullopt;
}
TextWithEntities ResolveRichButtonLabelDates(
TextWithEntities label,
const Ui::Text::FormattedDateFactory &dates) {
// BlockParser::checkEntities hands a FormattedDate entity its own internal
// index, createBlock promotes that index to the block's link index, and
// the renderer then paints such a block with the link pen and with an
// underlined font, and publishes a FormattedDateClickHandler for it. None
// of that belongs to a button label, which reads as one label in one font,
// one pen and one click target, so the date is resolved here exactly the
// way the parser would resolve it and the entity is dropped before layout.
// The list is searched from the front again after every replacement,
// because a normalized label orders its entities as a laminar family in
// post-order rather than by offset, and a textDate may itself contain a
// textCustomEmoji, whose entry the rewrite drops together with the text it
// covered — which is what the parser does today when it jumps past the
// date it substituted.
while (true) {
const auto i = ranges::find_if(label.entities, [](
const EntityInText &entity) {
return (entity.type() == EntityType::FormattedDate);
});
if (i == label.entities.end()) {
return label;
}
const auto offset = i->offset();
const auto length = i->length();
const auto till = offset + length;
const auto [date, flags] = DeserializeFormattedDateData(i->data());
if ((flags == FormattedDateFlags()) || !dates) {
label.entities.erase(i);
continue;
}
const auto replacement = dates(date, flags).text;
const auto delta = int(replacement.size()) - length;
label.text.replace(offset, length, replacement);
auto entities = EntitiesInText();
entities.reserve(label.entities.size());
for (const auto &entity : label.entities) {
if (&entity == &*i) {
continue;
}
auto updated = entity;
if (entity.offset() >= till) {
updated.shiftRight(delta);
} else if (entity.offset() + entity.length() <= offset) {
} else if ((entity.offset() <= offset)
&& (entity.offset() + entity.length() >= till)) {
updated.shrinkFromRight(-delta);
} else {
continue;
}
entities.push_back(updated);
}
label.entities = std::move(entities);
}
}
void ExpandInlineTextObjects(TextWithEntities *text, bool withIcons) {
auto &entities = text->entities;
for (auto i = entities.begin(); i != entities.end();) {
if (i->type() != EntityType::CustomEmoji) {
++i;
continue;
}
const auto object = ParseInlineTextObjectEntity(i->data());
if (!object) {
++i;
continue;
}
auto nested = EntitiesInText();
const auto replacement = v::match(object->data, [](
const InlineTextObjectFormulaData &data) {
return data.trimmedTex;
}, [](const InlineTextObjectIvImageData &data) {
return data.replacementText;
}, [&](const InlineTextObjectButtonData &data) {
nested = data.label.entities;
return data.label.text;
});
const auto offset = i->offset();
ReplaceInlineObjectText(text, *i, replacement);
const auto formula = (object->kind
== InlineTextObjectKind::Formula);
if (withIcons && formula && !replacement.isEmpty()) {
const auto icon = Ui::Text::IconEmoji(
&st::ivSummaryMathIcon,
replacement);
*i = EntityInText(
EntityType::CustomEmoji,
offset,
int(replacement.size()),
icon.entities.front().data());
++i;
} else {
i = ExpandInlineObjectEntities(text, i, nested);
}
}
}
bool TextHasInlineLinkButton(const TextWithEntities &text) {
return ranges::any_of(text.entities, [](const EntityInText &entity) {
return InlineLinkButtonFor(entity).has_value();
});
}
std::vector<InlineLinkButtonSpan> ExpandInlineLinkButtons(
TextWithEntities *text,
RichButtonLabelDates dates,
const Ui::Text::FormattedDateFactory &factory) {
auto result = std::vector<InlineLinkButtonSpan>();
auto &entities = text->entities;
for (auto i = entities.begin(); i != entities.end();) {
const auto button = InlineLinkButtonFor(*i);
if (!button) {
++i;
continue;
}
auto label = (dates == RichButtonLabelDates::Resolve)
? ResolveRichButtonLabelDates(button->label, factory)
: button->label;
const auto actionable = (button->type
!= HistoryMessageMarkupButton::Type::Disabled);
auto data = actionable ? i->data() : QString();
const auto offset = i->offset();
ReplaceInlineObjectText(text, *i, label.text);
i = ExpandInlineObjectEntities(text, i, label.entities);
if (actionable && !label.text.isEmpty()) {
result.push_back({
.offset = offset,
.length = int(label.text.size()),
.data = std::move(data),
});
}
}
return result;
}
TextWithEntities NormalizeRichButtonLabel(TextWithEntities text) {
ExpandInlineTextObjects(&text, false);
text.entities.erase(
ranges::remove_if(text.entities, [](const EntityInText &entity) {
const auto type = entity.type();
return (type != EntityType::CustomEmoji)
&& (type != EntityType::FormattedDate);
}),
text.entities.end());
for (auto i = text.entities.begin(); i != text.entities.end();) {
if (i->type() != EntityType::FormattedDate) {
++i;
continue;
}
auto [date, flags] = DeserializeFormattedDateData(i->data());
if (date <= 0
&& i->data().startsWith(Ui::InputField::kCustomDateTagStart)) {
date = int32(i->data().mid(
Ui::InputField::kCustomDateTagStart.size()).toInt());
}
if (date <= 0) {
i = text.entities.erase(i);
continue;
}
if (!flags) {
flags |= FormattedDateFlag::ShortDate;
}
*i = EntityInText(
EntityType::FormattedDate,
i->offset(),
i->length(),
SerializeFormattedDateData(date, flags));
++i;
}
TextUtilities::Trim(text);
return text;
}
QString InlineFormulaCopySource(const QString &source) {
return u"$"_q + source + u"$"_q;
}
MarkdownPrepareDimensions CaptureMarkdownPrepareDimensions() {
return CaptureMarkdownPrepareDimensions(st::defaultMarkdown);
}
MarkdownPrepareDimensions CaptureMarkdownPrepareDimensions(
const style::Markdown &st) {
auto result = MarkdownPrepareDimensions();
result.bodyTextSize = TextSizeForFormula(st.body);
result.headingTextSizes = {
TextSizeForFormula(st.heading1),
TextSizeForFormula(st.heading2),
TextSizeForFormula(st.heading3),
TextSizeForFormula(st.heading4),
TextSizeForFormula(st.heading5),
TextSizeForFormula(st.heading6),
};
result.tableHeaderTextSize = TextSizeForFormula(
st.table.headerStyle);
result.tableBodyTextSize = TextSizeForFormula(
st.table.bodyStyle);
result.displayMathTextSize = st.displayMath.textSize;
result.displayMathMaxRenderWidth = st.displayMath.maxRenderWidth;
result.displayMathMaxRenderHeight = st.displayMath.maxRenderHeight;
return result;
}
} // namespace Iv::Markdown