Files
AyuGramDesktop/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.cpp
T
2026-07-10 14:04:29 +04:00

650 lines
19 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_native_richtext.h"
struct GeoPointLocation;
#include "base/algorithm.h"
#include "data/data_location.h"
#include "iv/markdown/iv_markdown_prepare_links.h"
#include "iv/markdown/iv_markdown_prepare_serialize.h"
#include "ui/basic_click_handlers.h"
#include "history/history_location_manager.h"
#include <algorithm>
#include <limits>
#include <utility>
namespace Iv::Markdown {
namespace {
constexpr auto kDefaultMapWidth = 400;
constexpr auto kDefaultMapHeight = 200;
[[nodiscard]] uint64 GeneratePreparedBlockIdValue(
NativeIvPrepareState *state) {
return uint64(++state->nextGeneratedId);
}
[[nodiscard]] PreparedMediaBlockId GeneratePreparedMediaBlockId(
NativeIvPrepareState *state) {
return { .value = GeneratePreparedBlockIdValue(state) };
}
[[nodiscard]] uint64 CanonicalPhotoId(const Iv::RichPage::Block &data) {
return data.photoId;
}
[[nodiscard]] uint64 CanonicalPhotoId(
const Iv::RichPage::GroupedMediaItem &data) {
return data.photoId;
}
[[nodiscard]] uint64 CanonicalDocumentId(const Iv::RichPage::Block &data) {
return data.documentId;
}
[[nodiscard]] uint64 CanonicalDocumentId(
const Iv::RichPage::GroupedMediaItem &data) {
return data.documentId;
}
[[nodiscard]] int CanonicalWidth(const Iv::RichPage::Block &data) {
return data.width;
}
[[nodiscard]] int CanonicalWidth(
const Iv::RichPage::GroupedMediaItem &data) {
return data.width;
}
[[nodiscard]] int CanonicalHeight(const Iv::RichPage::Block &data) {
return data.height;
}
[[nodiscard]] int CanonicalHeight(
const Iv::RichPage::GroupedMediaItem &data) {
return data.height;
}
[[nodiscard]] bool PrepareNativeIvGroupedMediaItem(
const Iv::RichPage::GroupedMediaItem &item,
PreparedGroupedMediaItemData *result) {
switch (item.kind) {
case Iv::RichPage::BlockKind::Photo: {
const auto photoId = CanonicalPhotoId(item);
const auto width = CanonicalWidth(item);
const auto height = CanonicalHeight(item);
if (!photoId || width <= 0 || height <= 0) {
return false;
}
result->media.kind = PreparedMediaItemKind::Photo;
result->media.id = photoId;
result->media.width = width;
result->media.height = height;
result->media.spoiler = item.spoiler;
return true;
}
case Iv::RichPage::BlockKind::Video: {
const auto documentId = CanonicalDocumentId(item);
const auto width = CanonicalWidth(item);
const auto height = CanonicalHeight(item);
if (!documentId || width <= 0 || height <= 0) {
return false;
}
result->media.kind = PreparedMediaItemKind::Document;
result->media.id = documentId;
result->media.width = width;
result->media.height = height;
result->media.spoiler = item.spoiler;
return true;
}
default:
return false;
}
}
void SortPreparedIvRichText(PreparedIvRichText *text) {
SortEntities(&text->text);
}
[[nodiscard]] const QString &NativeIvLinkColorizedEntityData() {
static const auto result = [] {
auto value = QString();
value.reserve(2);
value.push_back(QChar(kNativeIvLinkSpecialColorIndex));
value.push_back(QChar(kNativeIvLinkSpecialColorIndex));
return value;
}();
return result;
}
[[nodiscard]] bool AddNativeIvPreparedLink(
TextWithEntities *text,
std::vector<PreparedLink> *links,
int from,
int length,
QString target,
uint64 webpageId = 0) {
if (!length || target.isEmpty()) {
return true;
}
const auto index = links->size() + 1;
if (index > std::numeric_limits<uint16>::max()) {
return true;
}
auto prepared = PreparedLink();
if (webpageId) {
prepared.index = uint16(index);
prepared.kind = PreparedLinkKind::InstantViewPage;
prepared.webpageId = webpageId;
NormalizePreparedUrlLink(&prepared, target);
} else {
prepared = ClassifiedLink(uint16(index), target, nullptr);
}
if (prepared.kind == PreparedLinkKind::RejectedRelative
|| prepared.kind == PreparedLinkKind::LocalFile) {
return true;
}
FinalizePreparedUrlLink(&prepared, QStringView(text->text).mid(from, length));
if (prepared.kind == PreparedLinkKind::InstantViewPage) {
text->entities.push_back(EntityInText(
EntityType::Colorized,
from,
length,
NativeIvLinkColorizedEntityData()));
}
text->entities.push_back(EntityInText(
EntityType::CustomUrl,
from,
length,
InternalLinkData(uint16(index))));
links->push_back(std::move(prepared));
return true;
}
void AddNativeIvBlockAnchor(
QString *blockAnchorId,
std::vector<QString> *blockAnchorIds,
QString anchorId) {
if (anchorId.isEmpty()) {
return;
}
if (blockAnchorId && blockAnchorId->isEmpty()) {
*blockAnchorId = std::move(anchorId);
return;
}
if (blockAnchorId && *blockAnchorId == anchorId) {
return;
}
if (blockAnchorIds
&& !ranges::contains(*blockAnchorIds, anchorId)) {
blockAnchorIds->push_back(std::move(anchorId));
}
}
[[nodiscard]] bool DropNativeIvClickHandlerEntity(EntityType type) {
switch (type) {
case EntityType::Mention:
case EntityType::Hashtag:
case EntityType::BotCommand:
case EntityType::Cashtag:
case EntityType::Url:
case EntityType::Email:
case EntityType::Phone:
case EntityType::BankCard:
case EntityType::CustomUrl:
case EntityType::MentionName:
case EntityType::FormattedDate:
return true;
default:
return false;
}
}
void RememberCanonicalInlineFormula(
const EntityInText &entity,
NativeIvPrepareState *state,
NativeIvRichTextContext context) {
if (entity.type() != EntityType::CustomEmoji) {
return;
}
const auto parsed = ParseInlineTextObjectEntity(entity.data());
if (!parsed || parsed->kind != InlineTextObjectKind::Formula) {
return;
}
const auto formula = std::get_if<InlineTextObjectFormulaData>(&parsed->data);
if (!formula) {
return;
}
(void)state->rememberFormula(
MathKind::Inline,
formula->trimmedTex,
context.textSize,
context.renderWidthCap,
context.renderHeightCap);
}
void AppendCanonicalNativeIvRichText(
const Iv::RichPage::RichText &text,
PreparedIvRichText *result,
NativeIvPrepareState *state,
NativeIvRichTextContext context) {
const auto shift = result->text.text.size();
result->text.text.append(text.text.text);
result->text.entities.reserve(
result->text.entities.size() + text.text.entities.size());
for (const auto &entity : text.text.entities) {
if (context.dropClickHandlers
&& DropNativeIvClickHandlerEntity(entity.type())) {
continue;
}
if (entity.type() == EntityType::CustomUrl) {
if (entity.offset() < 0
|| entity.length() <= 0
|| entity.offset() >= text.text.text.size()) {
continue;
}
const auto length = std::min(
entity.length(),
int(text.text.text.size()) - entity.offset());
const auto decoded = Iv::DecodeRichPageLinkUrl(entity.data());
(void)AddNativeIvPreparedLink(
&result->text,
&result->links,
shift + entity.offset(),
length,
decoded ? decoded->url : entity.data(),
decoded ? decoded->webpageId : 0);
continue;
}
RememberCanonicalInlineFormula(entity, state, context);
result->text.entities.push_back(EntityInText(
entity.type(),
entity.offset() + shift,
entity.length(),
entity.data()));
}
}
[[nodiscard]] int ScaleNativeIvFormulaCap(
int cap,
int textSize,
int baseTextSize) {
if (cap <= 0) {
return 0;
}
const auto numerator = int64(cap) * std::max(textSize, 1);
const auto denominator = std::max(baseTextSize, 1);
return std::max(int((numerator + denominator - 1) / denominator), 1);
}
[[nodiscard]] NativeIvRichTextContext ResolveNativeIvRichTextContext(
NativeIvPrepareState *state,
NativeIvRichTextContext context) {
const auto bodyTextSize = state->dimensions.bodyTextSize;
if (!context.textSize) {
context.textSize = bodyTextSize;
}
if (!context.renderWidthCap) {
context.renderWidthCap = ScaleNativeIvFormulaCap(
state->dimensions.displayMathMaxRenderWidth,
context.textSize,
state->dimensions.displayMathTextSize);
}
if (!context.renderHeightCap) {
context.renderHeightCap = ScaleNativeIvFormulaCap(
state->dimensions.displayMathMaxRenderHeight,
context.textSize,
state->dimensions.displayMathTextSize);
}
return context;
}
[[nodiscard]] bool PrepareNativeIvCaption(
const Iv::RichPage::RichText &caption,
PreparedIvRichText *result,
QString *blockAnchorId,
NativeIvPrepareState *state) {
return PrepareNativeIvRichText(
caption,
result,
blockAnchorId,
state);
}
[[nodiscard]] bool PrepareNativeIvCanonicalPlaceholderBlock(
QString label,
const Iv::RichPage::RichText &caption,
QString anchorId,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
auto prepared = PreparedIvRichText();
if (!PrepareNativeIvCaption(caption, &prepared, &anchorId, state)) {
return state->result.failure.failed()
? false
: PrepareNativeIvPlainPlaceholderBlock(std::move(label), result);
}
SortPreparedIvRichText(&prepared);
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Placeholder;
block.text = std::move(prepared.text);
block.links = std::move(prepared.links);
block.anchorId = std::move(anchorId);
block.anchorIds = std::move(prepared.anchorIds);
block.supplementary = true;
block.forceTextSegment = state->editMode;
block.placeholder.label = std::move(label);
block.placeholder.copyText = block.text.text.isEmpty()
? block.placeholder.label
: (block.placeholder.label + u"\n"_q + block.text.text);
result->push_back(std::move(block));
return true;
}
void ApplyEmptyMediaCaptionPlaceholder(
PreparedBlock *block,
NativeIvPrepareState *state) {
if (!state->editMode || !block->text.text.isEmpty()) {
return;
}
block->editPlaceholderText = u"Caption"_q;
}
} // namespace
bool PrepareNativeIvRichText(
const Iv::RichPage::RichText &text,
PreparedIvRichText *result,
QString *blockAnchorId,
NativeIvPrepareState *state,
NativeIvRichTextContext context) {
context = ResolveNativeIvRichTextContext(state, context);
AddNativeIvBlockAnchor(blockAnchorId, &result->anchorIds, text.anchorId);
for (const auto &anchorId : text.anchorIds) {
AddNativeIvBlockAnchor(blockAnchorId, &result->anchorIds, anchorId);
}
AppendCanonicalNativeIvRichText(text, result, state, context);
return true;
}
bool AppendPreparedIvRichBlock(
std::vector<PreparedBlock> *result,
PreparedBlockKind kind,
int headingLevel,
PreparedIvRichText prepared,
QString anchorId,
bool allowEmpty,
bool supplementary,
std::optional<PreparedEditBlockSource> editBlock,
std::optional<PreparedEditLeafSource> editLeaf) {
SortPreparedIvRichText(&prepared);
if (prepared.text.text.isEmpty() && !allowEmpty) {
return true;
}
auto block = PreparedBlock();
block.kind = kind;
block.headingLevel = headingLevel;
block.text = std::move(prepared.text);
block.links = std::move(prepared.links);
block.anchorId = std::move(anchorId);
block.anchorIds = std::move(prepared.anchorIds);
block.supplementary = supplementary;
block.editBlock = std::move(editBlock);
block.editLeaf = std::move(editLeaf);
result->push_back(std::move(block));
return true;
}
bool PrepareNativeIvPlainPlaceholderBlock(
QString label,
std::vector<PreparedBlock> *result) {
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Placeholder;
block.placeholder.label = std::move(label);
block.placeholder.copyText = block.placeholder.label;
result->push_back(std::move(block));
return true;
}
bool PrepareNativeIvPhotoBlock(
const Iv::RichPage::Block &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
if (!CanonicalPhotoId(data)
|| CanonicalWidth(data) <= 0
|| CanonicalHeight(data) <= 0) {
return state->editMode
? PrepareNativeIvCanonicalPlaceholderBlock(
u"Photo"_q,
data.caption,
data.anchorId,
result,
state)
: true;
}
auto caption = PreparedIvRichText();
auto anchorId = QString();
if (!PrepareNativeIvCaption(data.caption, &caption, &anchorId, state)) {
return false;
}
SortPreparedIvRichText(&caption);
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Photo;
block.text = std::move(caption.text);
block.links = std::move(caption.links);
block.anchorId = data.anchorId.isEmpty() ? std::move(anchorId) : data.anchorId;
block.anchorIds = std::move(caption.anchorIds);
block.supplementary = true;
block.forceTextSegment = state->editMode;
ApplyEmptyMediaCaptionPlaceholder(&block, state);
block.photo.id = GeneratePreparedMediaBlockId(state);
block.photo.photoId = CanonicalPhotoId(data);
block.photo.width = CanonicalWidth(data);
block.photo.height = CanonicalHeight(data);
block.photo.urlOverride = data.url;
block.photo.caption = block.text;
block.photo.spoiler = data.spoiler;
block.photo.viewerOpen = true;
result->push_back(std::move(block));
return true;
}
bool PrepareNativeIvVideoBlock(
const Iv::RichPage::Block &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
if (!CanonicalDocumentId(data)
|| CanonicalWidth(data) <= 0
|| CanonicalHeight(data) <= 0) {
return state->editMode
? PrepareNativeIvCanonicalPlaceholderBlock(
u"Video"_q,
data.caption,
data.anchorId,
result,
state)
: true;
}
auto caption = PreparedIvRichText();
auto anchorId = QString();
if (!PrepareNativeIvCaption(data.caption, &caption, &anchorId, state)) {
return false;
}
SortPreparedIvRichText(&caption);
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Video;
block.text = std::move(caption.text);
block.links = std::move(caption.links);
block.anchorId = data.anchorId.isEmpty() ? std::move(anchorId) : data.anchorId;
block.anchorIds = std::move(caption.anchorIds);
block.supplementary = true;
block.forceTextSegment = state->editMode;
ApplyEmptyMediaCaptionPlaceholder(&block, state);
block.video.id = GeneratePreparedMediaBlockId(state);
block.video.media.kind = PreparedMediaItemKind::Document;
block.video.media.id = CanonicalDocumentId(data);
block.video.media.width = CanonicalWidth(data);
block.video.media.height = CanonicalHeight(data);
block.video.media.spoiler = data.spoiler;
block.video.caption = block.text;
result->push_back(std::move(block));
return true;
}
bool PrepareNativeIvAudioBlock(
const Iv::RichPage::Block &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
if (!CanonicalDocumentId(data)) {
return state->editMode
? PrepareNativeIvCanonicalPlaceholderBlock(
u"Audio"_q,
data.caption,
data.anchorId,
result,
state)
: true;
}
auto caption = PreparedIvRichText();
auto anchorId = QString();
if (!PrepareNativeIvCaption(data.caption, &caption, &anchorId, state)) {
return false;
}
SortPreparedIvRichText(&caption);
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Audio;
block.text = std::move(caption.text);
block.links = std::move(caption.links);
block.anchorId = data.anchorId.isEmpty() ? std::move(anchorId) : data.anchorId;
block.anchorIds = std::move(caption.anchorIds);
block.supplementary = true;
block.forceTextSegment = state->editMode;
ApplyEmptyMediaCaptionPlaceholder(&block, state);
block.audio.id = GeneratePreparedMediaBlockId(state);
block.audio.documentId = CanonicalDocumentId(data);
block.audio.title = data.audioTitle;
block.audio.performer = data.audioPerformer;
block.audio.fileName = data.audioFileName;
block.audio.duration = data.audioDuration;
result->push_back(std::move(block));
return true;
}
bool PrepareNativeIvMapBlock(
const Iv::RichPage::Block &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
const auto width = (data.width > 0) ? data.width : kDefaultMapWidth;
const auto height = (data.height > 0) ? data.height : kDefaultMapHeight;
if (data.zoom <= 0 || (!data.accessHash && !state->editMode)) {
return state->editMode
? PrepareNativeIvCanonicalPlaceholderBlock(
u"Map"_q,
data.caption,
data.anchorId,
result,
state)
: true;
}
auto caption = PreparedIvRichText();
auto anchorId = QString();
if (!PrepareNativeIvCaption(data.caption, &caption, &anchorId, state)) {
return false;
}
SortPreparedIvRichText(&caption);
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Map;
block.text = std::move(caption.text);
block.links = std::move(caption.links);
block.anchorId = data.anchorId.isEmpty() ? std::move(anchorId) : data.anchorId;
block.anchorIds = std::move(caption.anchorIds);
block.supplementary = true;
block.forceTextSegment = state->editMode;
ApplyEmptyMediaCaptionPlaceholder(&block, state);
block.map.id = GeneratePreparedMediaBlockId(state);
block.map.latitude = data.latitude;
block.map.longitude = data.longitude;
block.map.accessHash = data.accessHash;
block.map.width = width;
block.map.height = height;
block.map.zoom = data.zoom;
block.map.url = LocationClickHandler::Url(Data::LocationPoint(
data.latitude,
data.longitude,
Data::LocationPoint::NoAccessHash));
result->push_back(std::move(block));
return true;
}
bool PrepareNativeIvChannelBlock(
const Iv::RichPage::Block &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
auto prepared = PreparedChannelBlockData();
prepared.channelId = data.channelId;
prepared.title = data.channelTitle;
prepared.username = data.username;
if (!prepared.channelId || prepared.title.isEmpty()) {
return true;
}
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Channel;
prepared.id = GeneratePreparedMediaBlockId(state);
block.channel = std::move(prepared);
result->push_back(std::move(block));
return true;
}
bool PrepareNativeIvGroupedMediaBlock(
const Iv::RichPage::Block &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
auto block = PreparedBlock();
block.kind = PreparedBlockKind::GroupedMedia;
block.groupedMedia.id = GeneratePreparedMediaBlockId(state);
block.groupedMedia.intent = (data.mediaIntent
== Iv::RichPage::GroupedMediaIntent::Slideshow)
? PreparedGroupedMediaIntent::Slideshow
: PreparedGroupedMediaIntent::Collage;
block.groupedMedia.items.reserve(data.mediaItems.size());
for (const auto &item : data.mediaItems) {
auto prepared = PreparedGroupedMediaItemData();
prepared.id = GeneratePreparedMediaBlockId(state);
if (!PrepareNativeIvGroupedMediaItem(item, &prepared)) {
continue;
}
block.groupedMedia.items.push_back(std::move(prepared));
}
if (block.groupedMedia.items.empty()) {
return true;
}
auto preparedCaption = PreparedIvRichText();
auto anchorId = QString();
if (!PrepareNativeIvCaption(
data.caption,
&preparedCaption,
&anchorId,
state)) {
return false;
}
SortPreparedIvRichText(&preparedCaption);
block.text = std::move(preparedCaption.text);
block.links = std::move(preparedCaption.links);
block.anchorId = data.anchorId.isEmpty() ? std::move(anchorId) : data.anchorId;
block.anchorIds = std::move(preparedCaption.anchorIds);
block.supplementary = true;
block.forceTextSegment = state->editMode;
ApplyEmptyMediaCaptionPlaceholder(&block, state);
block.groupedMedia.caption = block.text;
result->push_back(std::move(block));
return true;
}
} // namespace Iv::Markdown