mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-08-12 14:43:54 +00:00
Support IV-specific tags in the editor.
This commit is contained in:
@@ -403,7 +403,8 @@ Fn<bool(
|
||||
EditLinkAction action)> DefaultEditLinkCallback(
|
||||
std::shared_ptr<Main::SessionShow> show,
|
||||
not_null<Ui::InputField*> field,
|
||||
const style::InputField *fieldStyle) {
|
||||
const style::InputField *fieldStyle,
|
||||
Fn<QString(QString)> linkValidator) {
|
||||
const auto weak = base::make_weak(field);
|
||||
return [=](
|
||||
EditLinkSelection selection,
|
||||
@@ -413,7 +414,8 @@ Fn<bool(
|
||||
if (action == EditLinkAction::Check) {
|
||||
return (Ui::InputField::IsValidMarkdownLink(link)
|
||||
&& !TextUtilities::IsMentionLink(link))
|
||||
|| Ui::InputField::IsCustomDateLink(link);
|
||||
|| Ui::InputField::IsCustomDateLink(link)
|
||||
|| (linkValidator && !linkValidator(link).isEmpty());
|
||||
}
|
||||
if (Ui::InputField::IsCustomDateLink(link)) {
|
||||
const auto dateStr = link.mid(
|
||||
@@ -479,6 +481,9 @@ Fn<bool(
|
||||
strong->commitMarkdownLinkEdit(selection, text, link);
|
||||
}
|
||||
};
|
||||
const auto validateLink = linkValidator
|
||||
? linkValidator
|
||||
: Fn<QString(QString)>(qthelp::validate_url);
|
||||
show->showBox(Box(
|
||||
EditLinkBox,
|
||||
show,
|
||||
@@ -486,7 +491,7 @@ Fn<bool(
|
||||
link,
|
||||
std::move(callback),
|
||||
fieldStyle,
|
||||
qthelp::validate_url));
|
||||
validateLink));
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@@ -523,7 +528,11 @@ auto InitMessageFieldHandlers(MessageFieldHandlersArgs &&args)
|
||||
}));
|
||||
if (const auto &show = args.show) {
|
||||
field->setEditLinkCallback(
|
||||
DefaultEditLinkCallback(show, field, args.fieldStyle));
|
||||
DefaultEditLinkCallback(
|
||||
show,
|
||||
field,
|
||||
args.fieldStyle,
|
||||
args.linkValidator));
|
||||
field->setEditLanguageCallback(DefaultEditLanguageCallback(show));
|
||||
InitSpellchecker(show, field, args.fieldStyle != nullptr);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,8 @@ Fn<bool(
|
||||
Ui::InputField::EditLinkAction action)> DefaultEditLinkCallback(
|
||||
std::shared_ptr<Main::SessionShow> show,
|
||||
not_null<Ui::InputField*> field,
|
||||
const style::InputField *fieldStyle = nullptr);
|
||||
const style::InputField *fieldStyle = nullptr,
|
||||
Fn<QString(QString)> linkValidator = nullptr);
|
||||
Fn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(
|
||||
std::shared_ptr<Ui::Show> show);
|
||||
|
||||
@@ -77,6 +78,7 @@ struct MessageFieldHandlersArgs {
|
||||
Fn<bool()> customEmojiPaused;
|
||||
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji;
|
||||
const style::InputField *fieldStyle = nullptr;
|
||||
Fn<QString(QString)> linkValidator;
|
||||
base::flat_set<QString> allowMarkdownTags;
|
||||
};
|
||||
auto InitMessageFieldHandlers(MessageFieldHandlersArgs &&args)
|
||||
|
||||
@@ -2137,8 +2137,8 @@ void State::rebuildTextNodes(
|
||||
})
|
||||
: std::nullopt);
|
||||
}
|
||||
appendBlockTextNode(path, LeafKind::BlockCaption);
|
||||
rebuildTextNodes(block.blocks, BlockChildrenContainer(path));
|
||||
appendBlockTextNode(path, LeafKind::BlockCaption);
|
||||
break;
|
||||
case BlockKind::List:
|
||||
for (auto j = 0, itemCount = int(block.listItems.size());
|
||||
|
||||
@@ -0,0 +1,722 @@
|
||||
/*
|
||||
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/editor/iv_editor_text_entities.h"
|
||||
|
||||
#include "iv/markdown/iv_markdown_prepare_serialize.h"
|
||||
#include "iv/markdown/iv_markdown_prepare_links.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace Iv::Editor {
|
||||
namespace {
|
||||
|
||||
struct FormulaReplacement {
|
||||
int offset = 0;
|
||||
int length = 0;
|
||||
QString source;
|
||||
};
|
||||
|
||||
struct TextRange {
|
||||
int offset = 0;
|
||||
int length = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool RangeInsideText(
|
||||
const QString &text,
|
||||
int offset,
|
||||
int length) {
|
||||
return (offset >= 0)
|
||||
&& (length >= 0)
|
||||
&& (offset <= text.size())
|
||||
&& (length <= text.size() - offset);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsFormulaObjectSpan(
|
||||
const QString &text,
|
||||
const EntityInText &entity) {
|
||||
return (entity.length() == 1)
|
||||
&& RangeInsideText(text, entity.offset(), entity.length())
|
||||
&& (text[entity.offset()] == QChar::ObjectReplacementCharacter);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<Markdown::InlineTextObjectFormulaData>
|
||||
FormulaDataFromEntity(const EntityInText &entity) {
|
||||
if (entity.type() != EntityType::CustomEmoji) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto parsed = Markdown::ParseInlineTextObjectEntity(entity.data());
|
||||
if (!parsed || parsed->kind != Markdown::InlineTextObjectKind::Formula) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto formula = std::get_if<Markdown::InlineTextObjectFormulaData>(
|
||||
&parsed->data);
|
||||
return formula ? std::make_optional(*formula) : std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString EditorSourceForFormula(
|
||||
const Markdown::InlineTextObjectFormulaData &formula) {
|
||||
const auto stripDelimiters = [](QString result) {
|
||||
result = result.trimmed();
|
||||
if (result.size() >= 2
|
||||
&& result.front() == QChar('$')
|
||||
&& result.back() == QChar('$')) {
|
||||
result = result.mid(1, result.size() - 2).trimmed();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
if (!formula.trimmedTex.isEmpty()) {
|
||||
return Markdown::InlineFormulaCopySource(
|
||||
stripDelimiters(formula.trimmedTex));
|
||||
}
|
||||
return Markdown::InlineFormulaCopySource(
|
||||
stripDelimiters(formula.copySource));
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<EntityInText> AdjustEntityForReplacement(
|
||||
const EntityInText &entity,
|
||||
int from,
|
||||
int oldLength,
|
||||
int newLength) {
|
||||
const auto till = from + oldLength;
|
||||
const auto delta = newLength - oldLength;
|
||||
const auto begin = entity.offset();
|
||||
const auto end = begin + entity.length();
|
||||
|
||||
if (end <= from) {
|
||||
return entity;
|
||||
} else if (begin >= till) {
|
||||
return EntityInText(
|
||||
entity.type(),
|
||||
begin + delta,
|
||||
entity.length(),
|
||||
entity.data());
|
||||
} else if (begin <= from && end >= till) {
|
||||
const auto length = entity.length() + delta;
|
||||
if (length <= 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return EntityInText(entity.type(), begin, length, entity.data());
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<TextWithTags::Tag> AdjustTagForReplacement(
|
||||
const TextWithTags::Tag &tag,
|
||||
int from,
|
||||
int oldLength,
|
||||
int newLength) {
|
||||
const auto till = from + oldLength;
|
||||
const auto delta = newLength - oldLength;
|
||||
const auto begin = tag.offset;
|
||||
const auto end = begin + tag.length;
|
||||
|
||||
if (end <= from) {
|
||||
return tag;
|
||||
} else if (begin >= till) {
|
||||
return TextWithTags::Tag{
|
||||
.offset = begin + delta,
|
||||
.length = tag.length,
|
||||
.id = tag.id,
|
||||
};
|
||||
} else if (begin <= from && end >= till) {
|
||||
const auto length = tag.length + delta;
|
||||
if (length <= 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return TextWithTags::Tag{
|
||||
.offset = begin,
|
||||
.length = length,
|
||||
.id = tag.id,
|
||||
};
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString TagsWithoutIvEditorTags(QStringView tags) {
|
||||
auto result = QList<QStringView>();
|
||||
for (const auto &tag : TextUtilities::SplitTags(tags)) {
|
||||
if (!Ui::InputField::IsInstantViewEditorTag(tag)
|
||||
&& !tag.startsWith(QChar('#'))) {
|
||||
result.push_back(tag);
|
||||
}
|
||||
}
|
||||
return TextUtilities::JoinTag(result);
|
||||
}
|
||||
|
||||
[[nodiscard]] TextWithTags::Tags GenericTagsWithoutIvEditorTags(
|
||||
const TextWithTags::Tags &tags) {
|
||||
auto result = TextWithTags::Tags();
|
||||
result.reserve(tags.size());
|
||||
for (const auto &tag : tags) {
|
||||
auto filtered = TagsWithoutIvEditorTags(tag.id);
|
||||
if (!filtered.isEmpty()) {
|
||||
result.push_back({
|
||||
.offset = tag.offset,
|
||||
.length = tag.length,
|
||||
.id = filtered,
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsValidAnchorEntity(const EntityInText &entity) {
|
||||
if (entity.type() != EntityType::CustomUrl
|
||||
|| !Ui::InputField::IsInstantViewAnchorLink(entity.data())) {
|
||||
return false;
|
||||
}
|
||||
return !Markdown::NormalizeFragmentId(entity.data().mid(1)).isEmpty();
|
||||
}
|
||||
|
||||
void SortTags(TextWithTags::Tags *tags);
|
||||
|
||||
void AppendIvEntityTags(
|
||||
TextWithTags::Tags *tags,
|
||||
const EntitiesInText &entities) {
|
||||
for (const auto &entity : entities) {
|
||||
if (entity.length() <= 0) {
|
||||
continue;
|
||||
}
|
||||
switch (entity.type()) {
|
||||
case EntityType::Subscript:
|
||||
tags->push_back({
|
||||
.offset = entity.offset(),
|
||||
.length = entity.length(),
|
||||
.id = Ui::InputField::kTagIvSubscript,
|
||||
});
|
||||
break;
|
||||
case EntityType::Superscript:
|
||||
tags->push_back({
|
||||
.offset = entity.offset(),
|
||||
.length = entity.length(),
|
||||
.id = Ui::InputField::kTagIvSuperscript,
|
||||
});
|
||||
break;
|
||||
case EntityType::Marked:
|
||||
tags->push_back({
|
||||
.offset = entity.offset(),
|
||||
.length = entity.length(),
|
||||
.id = Ui::InputField::kTagIvMarked,
|
||||
});
|
||||
break;
|
||||
case EntityType::CustomUrl:
|
||||
if (IsValidAnchorEntity(entity)) {
|
||||
tags->push_back({
|
||||
.offset = entity.offset(),
|
||||
.length = entity.length(),
|
||||
.id = entity.data(),
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OverlayTag(
|
||||
TextWithTags::Tags *tags,
|
||||
const TextWithTags::Tag &overlay,
|
||||
const QString &text) {
|
||||
if (overlay.id.isEmpty()
|
||||
|| overlay.length <= 0
|
||||
|| !RangeInsideText(text, overlay.offset, overlay.length)) {
|
||||
return;
|
||||
}
|
||||
const auto from = overlay.offset;
|
||||
const auto till = from + overlay.length;
|
||||
auto coveredTill = from;
|
||||
auto result = TextWithTags::Tags();
|
||||
result.reserve(tags->size() + 3);
|
||||
|
||||
for (const auto &tag : *tags) {
|
||||
const auto tagFrom = tag.offset;
|
||||
const auto tagTill = tag.offset + tag.length;
|
||||
if (tagTill <= from) {
|
||||
result.push_back(tag);
|
||||
continue;
|
||||
} else if (tagFrom >= till) {
|
||||
if (coveredTill < till) {
|
||||
result.push_back({
|
||||
.offset = coveredTill,
|
||||
.length = till - coveredTill,
|
||||
.id = overlay.id,
|
||||
});
|
||||
coveredTill = till;
|
||||
}
|
||||
result.push_back(tag);
|
||||
continue;
|
||||
}
|
||||
if (tagFrom > coveredTill) {
|
||||
result.push_back({
|
||||
.offset = coveredTill,
|
||||
.length = tagFrom - coveredTill,
|
||||
.id = overlay.id,
|
||||
});
|
||||
coveredTill = tagFrom;
|
||||
}
|
||||
if (tagFrom < from) {
|
||||
result.push_back({
|
||||
.offset = tagFrom,
|
||||
.length = from - tagFrom,
|
||||
.id = tag.id,
|
||||
});
|
||||
}
|
||||
const auto middleFrom = std::max(tagFrom, from);
|
||||
const auto middleTill = std::min(tagTill, till);
|
||||
if (middleFrom < middleTill) {
|
||||
result.push_back({
|
||||
.offset = middleFrom,
|
||||
.length = middleTill - middleFrom,
|
||||
.id = TextUtilities::TagWithAdded(tag.id, overlay.id),
|
||||
});
|
||||
coveredTill = middleTill;
|
||||
}
|
||||
if (tagTill > till) {
|
||||
result.push_back({
|
||||
.offset = till,
|
||||
.length = tagTill - till,
|
||||
.id = tag.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (coveredTill < till) {
|
||||
result.push_back({
|
||||
.offset = coveredTill,
|
||||
.length = till - coveredTill,
|
||||
.id = overlay.id,
|
||||
});
|
||||
}
|
||||
SortTags(&result);
|
||||
*tags = TextUtilities::SimplifyTags(std::move(result));
|
||||
}
|
||||
|
||||
void OverlayTags(
|
||||
TextWithTags::Tags *tags,
|
||||
const TextWithTags::Tags &overlays,
|
||||
const QString &text) {
|
||||
for (const auto &overlay : overlays) {
|
||||
OverlayTag(tags, overlay, text);
|
||||
}
|
||||
}
|
||||
|
||||
void SubtractRange(
|
||||
std::vector<TextRange> *ranges,
|
||||
int from,
|
||||
int till) {
|
||||
auto result = std::vector<TextRange>();
|
||||
for (const auto &range : *ranges) {
|
||||
const auto rangeFrom = range.offset;
|
||||
const auto rangeTill = range.offset + range.length;
|
||||
if (rangeTill <= from || rangeFrom >= till) {
|
||||
result.push_back(range);
|
||||
continue;
|
||||
}
|
||||
if (rangeFrom < from) {
|
||||
result.push_back({
|
||||
.offset = rangeFrom,
|
||||
.length = from - rangeFrom,
|
||||
});
|
||||
}
|
||||
if (rangeTill > till) {
|
||||
result.push_back({
|
||||
.offset = till,
|
||||
.length = rangeTill - till,
|
||||
});
|
||||
}
|
||||
}
|
||||
*ranges = std::move(result);
|
||||
}
|
||||
|
||||
void RemoveRangesFromTags(
|
||||
TextWithTags::Tags *tags,
|
||||
const TextWithTags::Tags &removed) {
|
||||
auto result = TextWithTags::Tags();
|
||||
for (const auto &tag : *tags) {
|
||||
auto ranges = std::vector<TextRange>{ {
|
||||
.offset = tag.offset,
|
||||
.length = tag.length,
|
||||
} };
|
||||
for (const auto &remove : removed) {
|
||||
SubtractRange(
|
||||
&ranges,
|
||||
remove.offset,
|
||||
remove.offset + remove.length);
|
||||
}
|
||||
for (const auto &range : ranges) {
|
||||
if (range.length > 0) {
|
||||
result.push_back({
|
||||
.offset = range.offset,
|
||||
.length = range.length,
|
||||
.id = tag.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
SortTags(&result);
|
||||
*tags = TextUtilities::SimplifyTags(std::move(result));
|
||||
}
|
||||
|
||||
void SortTags(TextWithTags::Tags *tags) {
|
||||
std::sort(tags->begin(), tags->end(), [](const auto &a, const auto &b) {
|
||||
if (a.offset != b.offset) {
|
||||
return a.offset < b.offset;
|
||||
} else if (a.length != b.length) {
|
||||
return a.length < b.length;
|
||||
}
|
||||
return a.id < b.id;
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool TagContains(QStringView tags, QStringView tagId) {
|
||||
return TextUtilities::SplitTags(tags).contains(tagId);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool TagContainsOtherThan(QStringView tags, QStringView tagId) {
|
||||
for (const auto &tag : TextUtilities::SplitTags(tags)) {
|
||||
if (tag != tagId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void MergeRanges(std::vector<TextRange> *ranges);
|
||||
|
||||
[[nodiscard]] std::vector<TextRange> RangesContainingTagId(
|
||||
const TextWithTags::Tags &tags,
|
||||
QStringView tagId,
|
||||
const QString &text) {
|
||||
auto result = std::vector<TextRange>();
|
||||
result.reserve(tags.size());
|
||||
for (const auto &tag : tags) {
|
||||
if (tag.length <= 0
|
||||
|| !RangeInsideText(text, tag.offset, tag.length)
|
||||
|| !TagContains(tag.id, tagId)) {
|
||||
continue;
|
||||
}
|
||||
result.push_back({
|
||||
.offset = tag.offset,
|
||||
.length = tag.length,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] EntitiesInText ValidAnchorEntitiesFromTags(
|
||||
const TextWithTags::Tags &tags,
|
||||
const QString &text) {
|
||||
auto result = EntitiesInText();
|
||||
result.reserve(tags.size());
|
||||
for (const auto &tag : tags) {
|
||||
if (tag.length <= 0 || !RangeInsideText(text, tag.offset, tag.length)) {
|
||||
continue;
|
||||
}
|
||||
for (const auto &single : TextUtilities::SplitTags(tag.id)) {
|
||||
if (!Ui::InputField::IsInstantViewAnchorLink(single)) {
|
||||
continue;
|
||||
}
|
||||
auto link = single.toString();
|
||||
if (Markdown::NormalizeFragmentId(link).isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
result.push_back(EntityInText(
|
||||
EntityType::CustomUrl,
|
||||
tag.offset,
|
||||
tag.length,
|
||||
link));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<TextRange> MathRangesWithoutIntersections(
|
||||
const TextWithTags::Tags &tags,
|
||||
const QString &text) {
|
||||
auto result = std::vector<TextRange>();
|
||||
for (const auto &math : tags) {
|
||||
if (math.length <= 0
|
||||
|| !RangeInsideText(text, math.offset, math.length)
|
||||
|| !TagContains(math.id, Ui::InputField::kTagIvMath)) {
|
||||
continue;
|
||||
}
|
||||
auto ranges = std::vector<TextRange>{ {
|
||||
.offset = math.offset,
|
||||
.length = math.length,
|
||||
} };
|
||||
for (const auto &other : tags) {
|
||||
if (other.length <= 0
|
||||
|| !RangeInsideText(text, other.offset, other.length)
|
||||
|| !TagContainsOtherThan(
|
||||
other.id,
|
||||
Ui::InputField::kTagIvMath)) {
|
||||
continue;
|
||||
}
|
||||
SubtractRange(
|
||||
&ranges,
|
||||
other.offset,
|
||||
other.offset + other.length);
|
||||
}
|
||||
result.insert(result.end(), ranges.begin(), ranges.end());
|
||||
}
|
||||
MergeRanges(&result);
|
||||
return result;
|
||||
}
|
||||
|
||||
void AppendEntitiesForTagId(
|
||||
EntitiesInText *entities,
|
||||
const TextWithTags::Tags &tags,
|
||||
QStringView tagId,
|
||||
EntityType type,
|
||||
const QString &text) {
|
||||
for (const auto &range : RangesContainingTagId(tags, tagId, text)) {
|
||||
entities->push_back(EntityInText(type, range.offset, range.length));
|
||||
}
|
||||
}
|
||||
|
||||
void MergeRanges(std::vector<TextRange> *ranges) {
|
||||
std::sort(ranges->begin(), ranges->end(), [](const auto &a, const auto &b) {
|
||||
if (a.offset != b.offset) {
|
||||
return a.offset < b.offset;
|
||||
}
|
||||
return a.length < b.length;
|
||||
});
|
||||
|
||||
auto result = std::vector<TextRange>();
|
||||
result.reserve(ranges->size());
|
||||
for (const auto &range : *ranges) {
|
||||
if (result.empty()) {
|
||||
result.push_back(range);
|
||||
continue;
|
||||
}
|
||||
auto &last = result.back();
|
||||
const auto lastEnd = last.offset + last.length;
|
||||
const auto rangeEnd = range.offset + range.length;
|
||||
if (range.offset > lastEnd) {
|
||||
result.push_back(range);
|
||||
} else if (rangeEnd > lastEnd) {
|
||||
last.length = rangeEnd - last.offset;
|
||||
}
|
||||
}
|
||||
*ranges = std::move(result);
|
||||
}
|
||||
|
||||
void SortEntities(EntitiesInText *entities) {
|
||||
std::sort(
|
||||
entities->begin(),
|
||||
entities->end(),
|
||||
[](const auto &a, const auto &b) {
|
||||
if (a.offset() != b.offset()) {
|
||||
return a.offset() < b.offset();
|
||||
} else if (a.length() != b.length()) {
|
||||
return a.length() < b.length();
|
||||
} else if (a.type() != b.type()) {
|
||||
return int(a.type()) < int(b.type());
|
||||
}
|
||||
return a.data() < b.data();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RichTextEditorConversion ConvertRichTextToEditorTags(TextWithEntities text) {
|
||||
auto formulas = std::vector<FormulaReplacement>();
|
||||
auto entities = EntitiesInText();
|
||||
entities.reserve(text.entities.size());
|
||||
|
||||
for (const auto &entity : text.entities) {
|
||||
const auto formula = FormulaDataFromEntity(entity);
|
||||
if (formula && IsFormulaObjectSpan(text.text, entity)) {
|
||||
formulas.push_back({
|
||||
.offset = entity.offset(),
|
||||
.length = entity.length(),
|
||||
.source = EditorSourceForFormula(*formula),
|
||||
});
|
||||
} else {
|
||||
entities.push_back(entity);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(
|
||||
formulas.begin(),
|
||||
formulas.end(),
|
||||
[](const auto &a, const auto &b) {
|
||||
return a.offset > b.offset;
|
||||
});
|
||||
|
||||
auto mathTags = TextWithTags::Tags();
|
||||
auto replacements = std::vector<RichTextEditorOffsetReplacement>();
|
||||
replacements.reserve(formulas.size());
|
||||
|
||||
for (const auto &formula : formulas) {
|
||||
const auto newLength = formula.source.size();
|
||||
|
||||
for (auto i = entities.begin(); i != entities.end();) {
|
||||
if (const auto adjusted = AdjustEntityForReplacement(
|
||||
*i,
|
||||
formula.offset,
|
||||
formula.length,
|
||||
newLength)) {
|
||||
*i++ = *adjusted;
|
||||
} else {
|
||||
i = entities.erase(i);
|
||||
}
|
||||
}
|
||||
for (auto i = mathTags.begin(); i != mathTags.end();) {
|
||||
if (const auto adjusted = AdjustTagForReplacement(
|
||||
*i,
|
||||
formula.offset,
|
||||
formula.length,
|
||||
newLength)) {
|
||||
*i++ = *adjusted;
|
||||
} else {
|
||||
i = mathTags.erase(i);
|
||||
}
|
||||
}
|
||||
|
||||
text.text.replace(formula.offset, formula.length, formula.source);
|
||||
if (newLength > 0) {
|
||||
mathTags.push_back({
|
||||
.offset = formula.offset,
|
||||
.length = newLength,
|
||||
.id = Ui::InputField::kTagIvMath,
|
||||
});
|
||||
}
|
||||
replacements.push_back({
|
||||
.richOffset = formula.offset,
|
||||
.richLength = formula.length,
|
||||
.editorLength = newLength,
|
||||
});
|
||||
}
|
||||
|
||||
auto tags = TextUtilities::ConvertEntitiesToTextTags(entities);
|
||||
auto ivTags = TextWithTags::Tags();
|
||||
AppendIvEntityTags(&ivTags, entities);
|
||||
RemoveRangesFromTags(&tags, mathTags);
|
||||
RemoveRangesFromTags(&ivTags, mathTags);
|
||||
OverlayTags(&tags, ivTags, text.text);
|
||||
OverlayTags(&tags, mathTags, text.text);
|
||||
SortTags(&tags);
|
||||
tags = TextUtilities::SimplifyTags(tags);
|
||||
|
||||
std::sort(
|
||||
replacements.begin(),
|
||||
replacements.end(),
|
||||
[](const auto &a, const auto &b) {
|
||||
return a.richOffset < b.richOffset;
|
||||
});
|
||||
|
||||
return {
|
||||
.text = { text.text, tags },
|
||||
.replacements = replacements,
|
||||
};
|
||||
}
|
||||
|
||||
int MapRichTextOffsetToEditorOffset(
|
||||
const std::vector<RichTextEditorOffsetReplacement> &replacements,
|
||||
int offset) {
|
||||
auto delta = 0;
|
||||
for (const auto &replacement : replacements) {
|
||||
if (replacement.richLength <= 0) {
|
||||
continue;
|
||||
}
|
||||
const auto richStart = replacement.richOffset;
|
||||
const auto richEnd = richStart + replacement.richLength;
|
||||
const auto editorStart = richStart + delta;
|
||||
if (offset < richStart) {
|
||||
break;
|
||||
} else if (offset <= richEnd) {
|
||||
return editorStart
|
||||
+ ((offset == richEnd) ? replacement.editorLength : 0);
|
||||
}
|
||||
delta += replacement.editorLength - replacement.richLength;
|
||||
}
|
||||
return offset + delta;
|
||||
}
|
||||
|
||||
TextWithEntities ConvertEditorTagsToRichText(TextWithTags text) {
|
||||
auto entities = TextUtilities::ConvertTextTagsToEntities(
|
||||
GenericTagsWithoutIvEditorTags(text.tags));
|
||||
AppendEntitiesForTagId(
|
||||
&entities,
|
||||
text.tags,
|
||||
Ui::InputField::kTagIvMarked,
|
||||
EntityType::Marked,
|
||||
text.text);
|
||||
AppendEntitiesForTagId(
|
||||
&entities,
|
||||
text.tags,
|
||||
Ui::InputField::kTagIvSubscript,
|
||||
EntityType::Subscript,
|
||||
text.text);
|
||||
AppendEntitiesForTagId(
|
||||
&entities,
|
||||
text.tags,
|
||||
Ui::InputField::kTagIvSuperscript,
|
||||
EntityType::Superscript,
|
||||
text.text);
|
||||
for (const auto &entity : ValidAnchorEntitiesFromTags(
|
||||
text.tags,
|
||||
text.text)) {
|
||||
entities.push_back(entity);
|
||||
}
|
||||
|
||||
auto mathRanges = MathRangesWithoutIntersections(text.tags, text.text);
|
||||
for (auto i = mathRanges.rbegin(); i != mathRanges.rend(); ++i) {
|
||||
const auto source = text.text.mid(i->offset, i->length);
|
||||
auto trimmedSource = source.trimmed();
|
||||
if (trimmedSource.size() >= 2
|
||||
&& trimmedSource.front() == QChar('$')
|
||||
&& trimmedSource.back() == QChar('$')) {
|
||||
trimmedSource = trimmedSource
|
||||
.mid(1, trimmedSource.size() - 2)
|
||||
.trimmed();
|
||||
}
|
||||
if (trimmedSource.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
const auto entityData = Markdown::SerializeInlineTextObjectEntity({
|
||||
.kind = Markdown::InlineTextObjectKind::Formula,
|
||||
.data = Markdown::InlineTextObjectFormulaData{
|
||||
.copySource = Markdown::InlineFormulaCopySource(trimmedSource),
|
||||
.trimmedTex = trimmedSource,
|
||||
},
|
||||
});
|
||||
for (auto j = entities.begin(); j != entities.end();) {
|
||||
if (const auto adjusted = AdjustEntityForReplacement(
|
||||
*j,
|
||||
i->offset,
|
||||
i->length,
|
||||
1)) {
|
||||
*j++ = *adjusted;
|
||||
} else {
|
||||
j = entities.erase(j);
|
||||
}
|
||||
}
|
||||
text.text.replace(
|
||||
i->offset,
|
||||
i->length,
|
||||
QString(QChar::ObjectReplacementCharacter));
|
||||
entities.push_back(EntityInText(
|
||||
EntityType::CustomEmoji,
|
||||
i->offset,
|
||||
1,
|
||||
entityData));
|
||||
}
|
||||
|
||||
SortEntities(&entities);
|
||||
return {
|
||||
.text = text.text,
|
||||
.entities = entities,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Iv::Editor
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/text/text_entity.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace Iv::Editor {
|
||||
|
||||
struct RichTextEditorOffsetReplacement {
|
||||
int richOffset = 0;
|
||||
int richLength = 0;
|
||||
int editorLength = 0;
|
||||
};
|
||||
|
||||
struct RichTextEditorConversion {
|
||||
TextWithTags text;
|
||||
std::vector<RichTextEditorOffsetReplacement> replacements;
|
||||
};
|
||||
|
||||
[[nodiscard]] RichTextEditorConversion ConvertRichTextToEditorTags(
|
||||
TextWithEntities text);
|
||||
[[nodiscard]] int MapRichTextOffsetToEditorOffset(
|
||||
const std::vector<RichTextEditorOffsetReplacement> &replacements,
|
||||
int offset);
|
||||
[[nodiscard]] TextWithEntities ConvertEditorTagsToRichText(TextWithTags text);
|
||||
|
||||
} // namespace Iv::Editor
|
||||
@@ -7,12 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "iv/editor/iv_editor_widget.h"
|
||||
|
||||
#include "base/qthelp_url.h"
|
||||
#include "base/qt/qt_common_adapters.h"
|
||||
#include "chat_helpers/message_field.h"
|
||||
#include "data/data_msg_id.h"
|
||||
#include "data/data_types.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "iv/editor/iv_editor_text_entities.h"
|
||||
#include "iv/markdown/iv_markdown_article_paint.h"
|
||||
#include "iv/markdown/iv_markdown_prepare_links.h"
|
||||
#include "iv/markdown/iv_markdown_prepare_native_richtext.h"
|
||||
#include "spellcheck/spellcheck_highlight_syntax.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
@@ -156,6 +159,25 @@ void EnableQTextEditLineMetrics(style::Markdown &style) {
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString ValidateInstantViewEditorLink(QString link) {
|
||||
const auto normal = qthelp::validate_url(link);
|
||||
if (!normal.isEmpty()) {
|
||||
return normal;
|
||||
}
|
||||
link = link.trimmed();
|
||||
const auto hasPayload = [&](const QString &prefix) {
|
||||
return link.startsWith(prefix)
|
||||
&& !link.mid(prefix.size()).trimmed().isEmpty();
|
||||
};
|
||||
if (hasPayload(u"mailto:"_q)
|
||||
|| hasPayload(u"tel:"_q)
|
||||
|| (link.startsWith(u"#"_q)
|
||||
&& !Markdown::NormalizeFragmentId(link).isEmpty())) {
|
||||
return link;
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ImeEventProducesInput(
|
||||
const QInputMethodEvent &e,
|
||||
const QTextCursor &cursor) {
|
||||
@@ -1479,6 +1501,9 @@ const Widget::CachedInlineFieldStyle &Widget::inlineFieldStyleFor(
|
||||
auto key = inlineFieldStyleKey(data);
|
||||
auto textFg = data.textFg;
|
||||
auto ownedTextFg = std::shared_ptr<style::owned_color>();
|
||||
auto ownedTextMarkBg = std::make_shared<style::owned_color>(
|
||||
data.textMarkBg);
|
||||
auto textMarkBg = ownedTextMarkBg->color();
|
||||
if (_inlineFieldTextColorOverride
|
||||
&& data.textFg.get() == _inlineFieldTextColorOverride->color().get()) {
|
||||
ownedTextFg = std::make_shared<style::owned_color>(data.textFg->c);
|
||||
@@ -1498,6 +1523,7 @@ const Widget::CachedInlineFieldStyle &Widget::inlineFieldStyleFor(
|
||||
: data.textStyle->font;
|
||||
fieldStyle->style.lineHeight = data.lineHeight;
|
||||
fieldStyle->textFg = textFg;
|
||||
fieldStyle->textMarkBg = textMarkBg;
|
||||
fieldStyle->textAlign = data.align;
|
||||
fieldStyle->placeholderFont = fieldStyle->style.font;
|
||||
fieldStyle->placeholderAlign = data.align;
|
||||
@@ -1505,6 +1531,7 @@ const Widget::CachedInlineFieldStyle &Widget::inlineFieldStyleFor(
|
||||
.key = key,
|
||||
.style = std::move(fieldStyle),
|
||||
.ownedTextFg = std::move(ownedTextFg),
|
||||
.ownedTextMarkBg = std::move(ownedTextMarkBg),
|
||||
});
|
||||
return _fieldStyles.back();
|
||||
}
|
||||
@@ -1558,6 +1585,9 @@ Widget::InlineFieldStyleData Widget::normalizedInlineFieldStyle(
|
||||
.textFg = _inlineFieldTextColorOverride
|
||||
? _inlineFieldTextColorOverride->color()
|
||||
: (valid ? leafStyle.textColor : _articleStyle->textColor),
|
||||
.textMarkBg = valid
|
||||
? leafStyle.markBg
|
||||
: _articleStyle->textPalette.markBg->c,
|
||||
.align = valid ? leafStyle.align : style::al_left,
|
||||
.italic = valid ? leafStyle.italic : false,
|
||||
};
|
||||
@@ -1574,6 +1604,7 @@ Widget::InlineFieldStyleKey Widget::inlineFieldStyleKey(
|
||||
: textStyle->font,
|
||||
.lineHeight = data.lineHeight,
|
||||
.textFg = data.textFg,
|
||||
.textMarkBg = data.textMarkBg,
|
||||
.align = data.align,
|
||||
};
|
||||
}
|
||||
@@ -1601,6 +1632,7 @@ void Widget::setupInlineField() {
|
||||
not_null<DocumentData*> emoji) {
|
||||
return Data::AllowEmojiWithoutPremium(peer, emoji);
|
||||
};
|
||||
_field->setInstantViewEditorTagsEnabled(true);
|
||||
InitMessageFieldHandlers({
|
||||
.session = &_controller->session(),
|
||||
.show = _controller->uiShow(),
|
||||
@@ -1611,11 +1643,13 @@ void Widget::setupInlineField() {
|
||||
},
|
||||
.allowPremiumEmoji = allowPremiumEmoji,
|
||||
.fieldStyle = &_field->st(),
|
||||
.linkValidator = ValidateInstantViewEditorLink,
|
||||
});
|
||||
_field->setMimeDataHook(WrappedMessageFieldMimeHook(
|
||||
Ui::InputField::MimeDataHook(),
|
||||
_field.get()));
|
||||
} else {
|
||||
_field->setInstantViewEditorTagsEnabled(false);
|
||||
_field->setInstantReplacesEnabled(
|
||||
rpl::single(false),
|
||||
rpl::single(false));
|
||||
@@ -1737,24 +1771,30 @@ void Widget::activateTextOrdinal(
|
||||
ensureInlineFieldForSegment(segmentIndex);
|
||||
refreshInlineFieldPlaceholder();
|
||||
_settingField = true;
|
||||
auto cursorSelectionFrom = selectionFrom;
|
||||
auto cursorSelectionTo = selectionTo;
|
||||
if (_state->activeFieldMode() == State::FieldMode::Raw) {
|
||||
_field->setTextWithTags(
|
||||
{ _state->activeRawText(), {} },
|
||||
Ui::InputField::HistoryAction::Clear);
|
||||
_article->clearTextLeafHeightOverride();
|
||||
} else {
|
||||
const auto activeText = _state->activeText();
|
||||
const auto activeText = ConvertRichTextToEditorTags(
|
||||
_state->activeText());
|
||||
_field->setTextWithTags(
|
||||
{
|
||||
activeText.text,
|
||||
TextUtilities::ConvertEntitiesToTextTags(activeText.entities),
|
||||
},
|
||||
activeText.text,
|
||||
Ui::InputField::HistoryAction::Clear);
|
||||
cursorSelectionFrom = MapRichTextOffsetToEditorOffset(
|
||||
activeText.replacements,
|
||||
selectionFrom);
|
||||
cursorSelectionTo = MapRichTextOffsetToEditorOffset(
|
||||
activeText.replacements,
|
||||
selectionTo);
|
||||
}
|
||||
auto cursor = _field->textCursor();
|
||||
const auto size = int(_field->getLastText().size());
|
||||
const auto from = std::clamp(selectionFrom, 0, size);
|
||||
const auto to = std::clamp(selectionTo, 0, size);
|
||||
const auto from = std::clamp(cursorSelectionFrom, 0, size);
|
||||
const auto to = std::clamp(cursorSelectionTo, 0, size);
|
||||
cursor.setPosition(from);
|
||||
if (to != from) {
|
||||
cursor.setPosition(to, QTextCursor::KeepAnchor);
|
||||
@@ -1784,10 +1824,7 @@ void Widget::applyFieldTextToState() {
|
||||
return;
|
||||
}
|
||||
const auto text = _field->getTextWithAppliedMarkdown();
|
||||
_state->applyActiveText({
|
||||
.text = text.text,
|
||||
.entities = TextUtilities::ConvertTextTagsToEntities(text.tags),
|
||||
});
|
||||
_state->applyActiveText(ConvertEditorTagsToRichText(text));
|
||||
}
|
||||
|
||||
void Widget::hideInlineField() {
|
||||
|
||||
@@ -85,6 +85,7 @@ private:
|
||||
const style::TextStyle *textStyle = nullptr;
|
||||
int lineHeight = 0;
|
||||
style::color textFg;
|
||||
QColor textMarkBg;
|
||||
style::align align = style::al_left;
|
||||
bool italic = false;
|
||||
};
|
||||
@@ -93,6 +94,7 @@ private:
|
||||
style::font font;
|
||||
int lineHeight = 0;
|
||||
style::color textFg;
|
||||
QColor textMarkBg;
|
||||
style::align align = style::al_left;
|
||||
|
||||
friend inline bool operator==(
|
||||
@@ -101,6 +103,7 @@ private:
|
||||
return (a.font == b.font)
|
||||
&& (a.lineHeight == b.lineHeight)
|
||||
&& (a.textFg == b.textFg)
|
||||
&& (a.textMarkBg == b.textMarkBg)
|
||||
&& (a.align == b.align);
|
||||
}
|
||||
|
||||
@@ -115,6 +118,7 @@ private:
|
||||
InlineFieldStyleKey key;
|
||||
std::shared_ptr<style::InputField> style;
|
||||
std::shared_ptr<style::owned_color> ownedTextFg;
|
||||
std::shared_ptr<style::owned_color> ownedTextMarkBg;
|
||||
};
|
||||
|
||||
enum class DragSelectionMode {
|
||||
|
||||
@@ -38,6 +38,17 @@ using TaskState = RichPage::TaskState;
|
||||
|
||||
constexpr auto kDefaultMapWidth = 400;
|
||||
constexpr auto kDefaultMapHeight = 200;
|
||||
constexpr auto kNoEntityIndex = -1;
|
||||
|
||||
[[nodiscard]] QString FormulaTexFromSource(QString source) {
|
||||
source = source.trimmed();
|
||||
if (source.size() >= 2
|
||||
&& source.front() == QChar('$')
|
||||
&& source.back() == QChar('$')) {
|
||||
source = source.mid(1, source.size() - 2).trimmed();
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
struct SerializeContext {
|
||||
not_null<Main::Session*> session;
|
||||
@@ -250,24 +261,42 @@ struct SerializeContext {
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] const EntityInText *FindOuterEntityAt(
|
||||
[[nodiscard]] bool SkipEntityForRange(
|
||||
const std::vector<EntityInText> &entities,
|
||||
int index,
|
||||
int skipIndex) {
|
||||
if (index == skipIndex) {
|
||||
return true;
|
||||
} else if (skipIndex == kNoEntityIndex) {
|
||||
return false;
|
||||
}
|
||||
const auto &entity = entities[index];
|
||||
const auto &skip = entities[skipIndex];
|
||||
return (index < skipIndex)
|
||||
&& (entity.offset() == skip.offset())
|
||||
&& (entity.length() == skip.length());
|
||||
}
|
||||
|
||||
[[nodiscard]] int FindOuterEntityAt(
|
||||
const std::vector<EntityInText> &entities,
|
||||
int position,
|
||||
int till,
|
||||
const EntityInText *skip) {
|
||||
for (const auto &entity : entities) {
|
||||
if (&entity == skip) {
|
||||
int skipIndex) {
|
||||
const auto count = int(entities.size());
|
||||
for (auto index = 0; index != count; ++index) {
|
||||
const auto &entity = entities[index];
|
||||
if (SkipEntityForRange(entities, index, skipIndex)) {
|
||||
continue;
|
||||
}
|
||||
if (entity.offset() == position
|
||||
&& entity.offset() + entity.length() <= till) {
|
||||
return &entity;
|
||||
return index;
|
||||
}
|
||||
if (entity.offset() > position) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
return kNoEntityIndex;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<MTPRichText> SerializeRichTextRange(
|
||||
@@ -276,13 +305,14 @@ struct SerializeContext {
|
||||
int from,
|
||||
int till,
|
||||
SerializeContext *context,
|
||||
const EntityInText *skip);
|
||||
int skipIndex);
|
||||
|
||||
[[nodiscard]] std::optional<MTPRichText> SerializeRichTextEntity(
|
||||
const QString &text,
|
||||
const std::vector<EntityInText> &entities,
|
||||
const EntityInText &entity,
|
||||
int entityIndex,
|
||||
SerializeContext *context) {
|
||||
const auto &entity = entities[entityIndex];
|
||||
const auto from = entity.offset();
|
||||
const auto length = entity.length();
|
||||
const auto segment = text.mid(from, length);
|
||||
@@ -292,7 +322,7 @@ struct SerializeContext {
|
||||
from,
|
||||
from + length,
|
||||
context,
|
||||
&entity);
|
||||
entityIndex);
|
||||
if (!inner) {
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -362,9 +392,9 @@ struct SerializeContext {
|
||||
if (!formula) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto source = !formula->copySource.isEmpty()
|
||||
? formula->copySource
|
||||
: formula->trimmedTex;
|
||||
const auto source = !formula->trimmedTex.isEmpty()
|
||||
? formula->trimmedTex
|
||||
: FormulaTexFromSource(formula->copySource);
|
||||
return source.isEmpty()
|
||||
? std::optional<MTPRichText>(
|
||||
MakePlainRichText(segment))
|
||||
@@ -446,13 +476,15 @@ struct SerializeContext {
|
||||
int from,
|
||||
int till,
|
||||
SerializeContext *context,
|
||||
const EntityInText *skip) {
|
||||
int skipIndex) {
|
||||
auto parts = QVector<MTPRichText>();
|
||||
auto position = from;
|
||||
while (position < till) {
|
||||
auto nextEntityStart = till;
|
||||
for (const auto &entity : entities) {
|
||||
if (&entity == skip) {
|
||||
const auto count = int(entities.size());
|
||||
for (auto index = 0; index != count; ++index) {
|
||||
const auto &entity = entities[index];
|
||||
if (SkipEntityForRange(entities, index, skipIndex)) {
|
||||
continue;
|
||||
}
|
||||
if (entity.offset() >= position
|
||||
@@ -471,8 +503,8 @@ struct SerializeContext {
|
||||
entities,
|
||||
position,
|
||||
till,
|
||||
skip);
|
||||
if (!entity) {
|
||||
skipIndex);
|
||||
if (entity == kNoEntityIndex) {
|
||||
parts.push_back(MakePlainRichText(text.mid(position, 1)));
|
||||
++position;
|
||||
continue;
|
||||
@@ -480,13 +512,14 @@ struct SerializeContext {
|
||||
const auto wrapped = SerializeRichTextEntity(
|
||||
text,
|
||||
entities,
|
||||
*entity,
|
||||
entity,
|
||||
context);
|
||||
if (!wrapped) {
|
||||
return std::nullopt;
|
||||
}
|
||||
parts.push_back(*wrapped);
|
||||
position = entity->offset() + entity->length();
|
||||
const auto &wrappedEntity = entities[entity];
|
||||
position = wrappedEntity.offset() + wrappedEntity.length();
|
||||
}
|
||||
return JoinRichTextParts(std::move(parts));
|
||||
}
|
||||
@@ -502,7 +535,7 @@ struct SerializeContext {
|
||||
0,
|
||||
text.text.text.size(),
|
||||
context,
|
||||
nullptr);
|
||||
kNoEntityIndex);
|
||||
if (!result) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,16 @@ using TableRow = RichPage::TableRow;
|
||||
using TableVerticalAlignment = RichPage::TableVerticalAlignment;
|
||||
using TaskState = RichPage::TaskState;
|
||||
|
||||
[[nodiscard]] QString FormulaTexFromSource(QString source) {
|
||||
source = source.trimmed();
|
||||
if (source.size() >= 2
|
||||
&& source.front() == QChar('$')
|
||||
&& source.back() == QChar('$')) {
|
||||
source = source.mid(1, source.size() - 2).trimmed();
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
const auto PhotoLargeLevels = u"ydxcwmbsa"_q;
|
||||
constexpr auto kDefaultMapWidth = 400;
|
||||
constexpr auto kDefaultMapHeight = 200;
|
||||
@@ -423,12 +433,12 @@ void RememberWebPageMedia(
|
||||
entityData));
|
||||
return true;
|
||||
}, [&](const MTPDtextMath &data) {
|
||||
const auto source = qs(data.vsource());
|
||||
const auto source = FormulaTexFromSource(qs(data.vsource()));
|
||||
const auto entityData = Markdown::SerializeInlineTextObjectEntity({
|
||||
.kind = Markdown::InlineTextObjectKind::Formula,
|
||||
.data = Markdown::InlineTextObjectFormulaData{
|
||||
.copySource = Markdown::InlineFormulaCopySource(source),
|
||||
.trimmedTex = source.trimmed(),
|
||||
.trimmedTex = source,
|
||||
},
|
||||
});
|
||||
if (entityData.isEmpty()) {
|
||||
|
||||
@@ -180,6 +180,15 @@ void RestoreRelatedArticleImageStates(
|
||||
return segment.isTextLeaf() || IsDisplayMathSegment(segment);
|
||||
}
|
||||
|
||||
[[nodiscard]] QColor MarkBgColorForStyle(const style::Markdown &st) {
|
||||
auto result = st.textPalette.markBg->c;
|
||||
result.setAlphaF(result.alphaF() * std::clamp(
|
||||
st.markBgOpacity,
|
||||
0.,
|
||||
1.));
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
[[nodiscard]] int CompareValues(const T &a, const T &b) {
|
||||
return (a < b) ? -1 : (b < a) ? 1 : 0;
|
||||
@@ -2084,11 +2093,7 @@ void MarkdownArticle::Impl::paint(
|
||||
local.selectionState.segments = &_segments;
|
||||
const auto &paintSt = local.paintMarkdownStyle(st);
|
||||
auto textPalette = paintSt.textPalette;
|
||||
auto markBg = textPalette.markBg->c;
|
||||
markBg.setAlphaF(markBg.alphaF() * std::clamp(
|
||||
paintSt.markBgOpacity,
|
||||
0.,
|
||||
1.));
|
||||
auto markBg = MarkBgColorForStyle(paintSt);
|
||||
const auto ownedMarkBg = style::internal::OwnedColor(markBg);
|
||||
textPalette.markBg = ownedMarkBg.color();
|
||||
const auto &previousTextPalette = p.textPalette();
|
||||
@@ -2305,6 +2310,7 @@ MarkdownArticleTextLeafStyle MarkdownArticle::Impl::textLeafStyleForSegment(
|
||||
return {
|
||||
.textStyle = &textStyle,
|
||||
.textColor = TextColorForSegment(*segment, st),
|
||||
.markBg = MarkBgColorForStyle(st),
|
||||
.lineHeight = TextLineHeight(textStyle),
|
||||
.align = segment->align,
|
||||
.italic = segment->block && segment->block->pullquote,
|
||||
@@ -2325,6 +2331,7 @@ MarkdownArticleTextLeafStyle MarkdownArticle::Impl::editableStyleForSegment(
|
||||
return {
|
||||
.textStyle = &st.displayMath.fallbackStyle,
|
||||
.textColor = st.displayMath.fg,
|
||||
.markBg = MarkBgColorForStyle(st),
|
||||
.lineHeight = TextLineHeight(st.displayMath.fallbackStyle),
|
||||
.align = segment->align,
|
||||
};
|
||||
|
||||
@@ -20,6 +20,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/click_handler.h"
|
||||
#include "ui/painter.h"
|
||||
|
||||
#include <QtGui/QColor>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
@@ -250,6 +252,7 @@ inline bool operator!=(
|
||||
struct MarkdownArticleTextLeafStyle {
|
||||
const style::TextStyle *textStyle = nullptr;
|
||||
style::color textColor;
|
||||
QColor markBg;
|
||||
int lineHeight = 0;
|
||||
style::align align = style::al_left;
|
||||
bool italic = false;
|
||||
|
||||
@@ -1094,20 +1094,16 @@ auto InlineFormulaObjectCache::lookupOrCreate(
|
||||
if (measuredData) {
|
||||
measured = *measuredData;
|
||||
} else {
|
||||
const auto fallbackAscent = std::max(TextLineAscent(textStyle), 0);
|
||||
const auto fallbackSize = QSize(
|
||||
std::max(textStyle.font->width(signature.trimmedTex), 1),
|
||||
std::max(TextLineHeight(textStyle), 1));
|
||||
measured.logicalSize = fallbackSize;
|
||||
measured.logicalDepth = std::max(
|
||||
fallbackSize.height() - fallbackAscent,
|
||||
0);
|
||||
measured.exact = InlineFormulaExactMetricsFromLogical(
|
||||
fallbackSize,
|
||||
fallbackAscent);
|
||||
measured.fallbackText = signature.trimmedTex;
|
||||
measured.success = false;
|
||||
NormalizeInlineFormulaRasterMetrics(&measured);
|
||||
if (!_renderer) {
|
||||
_renderer = std::make_shared<MathRenderer>();
|
||||
}
|
||||
measured = _renderer->measureFormula({
|
||||
.trimmedTex = signature.trimmedTex,
|
||||
.kind = signature.kind,
|
||||
.textSize = signature.textSize,
|
||||
.renderWidthCap = signature.renderWidthCap,
|
||||
.renderHeightCap = signature.renderHeightCap,
|
||||
});
|
||||
measuredData = std::make_shared<MeasuredFormula>(measured);
|
||||
}
|
||||
const auto fallbackText = InlineFormulaDisplayFallbackText(
|
||||
|
||||
@@ -15,6 +15,8 @@ PRIVATE
|
||||
iv/editor/iv_editor_box.h
|
||||
iv/editor/iv_editor_state.cpp
|
||||
iv/editor/iv_editor_state.h
|
||||
iv/editor/iv_editor_text_entities.cpp
|
||||
iv/editor/iv_editor_text_entities.h
|
||||
iv/editor/iv_editor_widget.cpp
|
||||
iv/editor/iv_editor_widget.h
|
||||
|
||||
|
||||
+1
-1
Submodule Telegram/lib_spellcheck updated: fbc99868e1...0ebd04ece2
Reference in New Issue
Block a user