mirror of
https://github.com/vale981/tdesktop
synced 2025-03-06 02:01:40 -05:00
Support markdown replaces in Ui::InputField.
This commit is contained in:
parent
017ec87d60
commit
6f6ec217e3
8 changed files with 902 additions and 544 deletions
|
@ -56,11 +56,25 @@ EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) {
|
||||||
|
|
||||||
result.reserve(tags.size());
|
result.reserve(tags.size());
|
||||||
auto mentionStart = qstr("mention://user.");
|
auto mentionStart = qstr("mention://user.");
|
||||||
for_const (auto &tag, tags) {
|
for (const auto &tag : tags) {
|
||||||
|
const auto push = [&](
|
||||||
|
EntityInTextType type,
|
||||||
|
const QString &data = QString()) {
|
||||||
|
result.push_back(
|
||||||
|
EntityInText(type, tag.offset, tag.length, data));
|
||||||
|
};
|
||||||
if (tag.id.startsWith(mentionStart)) {
|
if (tag.id.startsWith(mentionStart)) {
|
||||||
if (auto match = qthelp::regex_match("^(\\d+\\.\\d+)(/|$)", tag.id.midRef(mentionStart.size()))) {
|
if (auto match = qthelp::regex_match("^(\\d+\\.\\d+)(/|$)", tag.id.midRef(mentionStart.size()))) {
|
||||||
result.push_back(EntityInText(EntityInTextMentionName, tag.offset, tag.length, match->captured(1)));
|
push(EntityInTextMentionName, match->captured(1));
|
||||||
}
|
}
|
||||||
|
} else if (tag.id == Ui::InputField::kTagBold) {
|
||||||
|
push(EntityInTextBold);
|
||||||
|
} else if (tag.id == Ui::InputField::kTagItalic) {
|
||||||
|
push(EntityInTextItalic);
|
||||||
|
} else if (tag.id == Ui::InputField::kTagCode) {
|
||||||
|
push(EntityInTextCode);
|
||||||
|
} else if (tag.id == Ui::InputField::kTagPre) {
|
||||||
|
push(EntityInTextPre);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
@ -73,12 +87,21 @@ TextWithTags::Tags ConvertEntitiesToTextTags(const EntitiesInText &entities) {
|
||||||
}
|
}
|
||||||
|
|
||||||
result.reserve(entities.size());
|
result.reserve(entities.size());
|
||||||
for_const (auto &entity, entities) {
|
for (const auto &entity : entities) {
|
||||||
if (entity.type() == EntityInTextMentionName) {
|
const auto push = [&](const QString &tag) {
|
||||||
|
result.push_back({ entity.offset(), entity.length(), tag });
|
||||||
|
};
|
||||||
|
switch (entity.type()) {
|
||||||
|
case EntityInTextMentionName: {
|
||||||
auto match = QRegularExpression("^(\\d+\\.\\d+)$").match(entity.data());
|
auto match = QRegularExpression("^(\\d+\\.\\d+)$").match(entity.data());
|
||||||
if (match.hasMatch()) {
|
if (match.hasMatch()) {
|
||||||
result.push_back({ entity.offset(), entity.length(), qstr("mention://user.") + entity.data() });
|
push(qstr("mention://user.") + entity.data());
|
||||||
}
|
}
|
||||||
|
} break;
|
||||||
|
case EntityInTextBold: push(Ui::InputField::kTagBold); break;
|
||||||
|
case EntityInTextItalic: push(Ui::InputField::kTagItalic); break;
|
||||||
|
case EntityInTextCode: push(Ui::InputField::kTagCode); break;
|
||||||
|
case EntityInTextPre: push(Ui::InputField::kTagPre); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
@ -119,15 +142,16 @@ void InitMessageField(not_null<Ui::InputField*> field) {
|
||||||
field->setTagMimeProcessor(std::make_unique<FieldTagMimeProcessor>());
|
field->setTagMimeProcessor(std::make_unique<FieldTagMimeProcessor>());
|
||||||
|
|
||||||
field->document()->setDocumentMargin(4.);
|
field->document()->setDocumentMargin(4.);
|
||||||
const auto additional = convertScale(4) - 4;
|
field->setAdditionalMargin(convertScale(4) - 4);
|
||||||
field->rawTextEdit()->setStyleSheet(
|
|
||||||
qsl("QTextEdit { margin: %1px; }").arg(additional));
|
|
||||||
|
|
||||||
|
field->customTab(true);
|
||||||
field->setInstantReplaces(Ui::InstantReplaces::Default());
|
field->setInstantReplaces(Ui::InstantReplaces::Default());
|
||||||
field->enableInstantReplaces(Global::ReplaceEmoji());
|
field->enableInstantReplaces(Global::ReplaceEmoji());
|
||||||
|
field->enableMarkdownSupport(Global::ReplaceEmoji());
|
||||||
auto &changed = Global::RefReplaceEmojiChanged();
|
auto &changed = Global::RefReplaceEmojiChanged();
|
||||||
Ui::AttachAsChild(field, changed.add_subscription([=] {
|
Ui::AttachAsChild(field, changed.add_subscription([=] {
|
||||||
field->enableInstantReplaces(Global::ReplaceEmoji());
|
field->enableInstantReplaces(Global::ReplaceEmoji());
|
||||||
|
field->enableMarkdownSupport(Global::ReplaceEmoji());
|
||||||
}));
|
}));
|
||||||
field->window()->activateWindow();
|
field->window()->activateWindow();
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,8 +45,13 @@ Draft::Draft(
|
||||||
|
|
||||||
void applyPeerCloudDraft(PeerId peerId, const MTPDdraftMessage &draft) {
|
void applyPeerCloudDraft(PeerId peerId, const MTPDdraftMessage &draft) {
|
||||||
auto history = App::history(peerId);
|
auto history = App::history(peerId);
|
||||||
auto text = TextWithEntities { qs(draft.vmessage), draft.has_entities() ? TextUtilities::EntitiesFromMTP(draft.ventities.v) : EntitiesInText() };
|
auto textWithTags = TextWithTags {
|
||||||
auto textWithTags = TextWithTags { TextUtilities::ApplyEntities(text), ConvertEntitiesToTextTags(text.entities) };
|
qs(draft.vmessage),
|
||||||
|
ConvertEntitiesToTextTags(
|
||||||
|
draft.has_entities()
|
||||||
|
? TextUtilities::EntitiesFromMTP(draft.ventities.v)
|
||||||
|
: EntitiesInText())
|
||||||
|
};
|
||||||
auto replyTo = draft.has_reply_to_msg_id() ? draft.vreply_to_msg_id.v : MsgId(0);
|
auto replyTo = draft.has_reply_to_msg_id() ? draft.vreply_to_msg_id.v : MsgId(0);
|
||||||
auto cloudDraft = std::make_unique<Draft>(textWithTags, replyTo, MessageCursor(QFIXED_MAX, QFIXED_MAX, QFIXED_MAX), draft.is_no_webpage());
|
auto cloudDraft = std::make_unique<Draft>(textWithTags, replyTo, MessageCursor(QFIXED_MAX, QFIXED_MAX, QFIXED_MAX), draft.is_no_webpage());
|
||||||
cloudDraft->date = draft.vdate.v;
|
cloudDraft->date = draft.vdate.v;
|
||||||
|
|
|
@ -5836,7 +5836,7 @@ void HistoryWidget::editMessage(not_null<HistoryItem*> item) {
|
||||||
|
|
||||||
const auto original = item->originalText();
|
const auto original = item->originalText();
|
||||||
const auto editData = TextWithTags {
|
const auto editData = TextWithTags {
|
||||||
TextUtilities::ApplyEntities(original),
|
original.text,
|
||||||
ConvertEntitiesToTextTags(original.entities)
|
ConvertEntitiesToTextTags(original.entities)
|
||||||
};
|
};
|
||||||
const auto cursor = MessageCursor {
|
const auto cursor = MessageCursor {
|
||||||
|
@ -6235,7 +6235,10 @@ void HistoryWidget::onCancel() {
|
||||||
onInlineBotCancel();
|
onInlineBotCancel();
|
||||||
} else if (_editMsgId) {
|
} else if (_editMsgId) {
|
||||||
auto original = _replyEditMsg ? _replyEditMsg->originalText() : TextWithEntities();
|
auto original = _replyEditMsg ? _replyEditMsg->originalText() : TextWithEntities();
|
||||||
auto editData = TextWithTags { TextUtilities::ApplyEntities(original), ConvertEntitiesToTextTags(original.entities) };
|
auto editData = TextWithTags {
|
||||||
|
original.text,
|
||||||
|
ConvertEntitiesToTextTags(original.entities)
|
||||||
|
};
|
||||||
if (_replyEditMsg && editData != _field->getTextWithTags()) {
|
if (_replyEditMsg && editData != _field->getTextWithTags()) {
|
||||||
Ui::show(Box<ConfirmBox>(
|
Ui::show(Box<ConfirmBox>(
|
||||||
lang(lng_cancel_edit_post_sure),
|
lang(lng_cancel_edit_post_sure),
|
||||||
|
|
|
@ -234,7 +234,9 @@ public:
|
||||||
if (flags & flag) {
|
if (flags & flag) {
|
||||||
createBlock();
|
createBlock();
|
||||||
flags &= ~flag;
|
flags &= ~flag;
|
||||||
if (flag == TextBlockFPre) {
|
if (flag == TextBlockFPre
|
||||||
|
&& !_t->_blocks.empty()
|
||||||
|
&& _t->_blocks.back()->type() != TextBlockTNewline) {
|
||||||
newlineAwaited = true;
|
newlineAwaited = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,12 +30,39 @@ QString ExpressionMailNameAtEnd() {
|
||||||
return qsl("[a-zA-Z\\-_\\.0-9]{1,256}$");
|
return qsl("[a-zA-Z\\-_\\.0-9]{1,256}$");
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ExpressionSeparators(const QString &additional) {
|
QString Quotes() {
|
||||||
// UTF8 quotes and ellipsis
|
// UTF8 quotes and ellipsis
|
||||||
const auto quotes = QString::fromUtf8("\xC2\xAB\xC2\xBB\xE2\x80\x9C\xE2\x80\x9D\xE2\x80\x98\xE2\x80\x99\xE2\x80\xA6");
|
return QString::fromUtf8("\xC2\xAB\xC2\xBB\xE2\x80\x9C\xE2\x80\x9D\xE2\x80\x98\xE2\x80\x99\xE2\x80\xA6");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ExpressionSeparators(const QString &additional) {
|
||||||
|
static const auto quotes = Quotes();
|
||||||
return qsl("\\s\\.,:;<>|'\"\\[\\]\\{\\}\\~\\!\\?\\%\\^\\(\\)\\-\\+=\\x10") + quotes + additional;
|
return qsl("\\s\\.,:;<>|'\"\\[\\]\\{\\}\\~\\!\\?\\%\\^\\(\\)\\-\\+=\\x10") + quotes + additional;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString Separators(const QString &additional) {
|
||||||
|
static const auto quotes = Quotes();
|
||||||
|
return qsl(" \x10\n\r\t.,:;<>|'\"[]{}~!?%^()-+=")
|
||||||
|
+ QChar(0xfdd0) // QTextBeginningOfFrame
|
||||||
|
+ QChar(0xfdd1) // QTextEndOfFrame
|
||||||
|
+ QChar(QChar::ParagraphSeparator)
|
||||||
|
+ QChar(QChar::LineSeparator)
|
||||||
|
+ quotes
|
||||||
|
+ additional;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SeparatorsBold() {
|
||||||
|
return Separators(qsl("`/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SeparatorsItalic() {
|
||||||
|
return Separators(qsl("`*/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SeparatorsMono() {
|
||||||
|
return Separators(qsl("*/"));
|
||||||
|
}
|
||||||
|
|
||||||
QString ExpressionHashtag() {
|
QString ExpressionHashtag() {
|
||||||
return qsl("(^|[") + ExpressionSeparators(qsl("`\\*/")) + qsl("])#[\\w]{2,64}([\\W]|$)");
|
return qsl("(^|[") + ExpressionSeparators(qsl("`\\*/")) + qsl("])#[\\w]{2,64}([\\W]|$)");
|
||||||
}
|
}
|
||||||
|
@ -52,28 +79,12 @@ QString ExpressionBotCommand() {
|
||||||
return qsl("(^|[") + ExpressionSeparators(qsl("`\\*")) + qsl("])/[A-Za-z_0-9]{1,64}(@[A-Za-z_0-9]{5,32})?([\\W]|$)");
|
return qsl("(^|[") + ExpressionSeparators(qsl("`\\*")) + qsl("])/[A-Za-z_0-9]{1,64}(@[A-Za-z_0-9]{5,32})?([\\W]|$)");
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ExpressionMarkdownBold() {
|
|
||||||
auto separators = ExpressionSeparators(qsl("`/"));
|
|
||||||
return qsl("(^|[") + separators + qsl("])(\\*\\*)[\\s\\S]+?(\\*\\*)([") + separators + qsl("]|$)");
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ExpressionMarkdownItalic() {
|
|
||||||
auto separators = ExpressionSeparators(qsl("`\\*/"));
|
|
||||||
return qsl("(^|[") + separators + qsl("])(__)[\\s\\S]+?(__)([") + separators + qsl("]|$)");
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ExpressionMarkdownMonoInline() { // code
|
|
||||||
auto separators = ExpressionSeparators(qsl("\\*/"));
|
|
||||||
return qsl("(^|[") + separators + qsl("])(`)[^\\n]+?(`)([") + separators + qsl("]|$)");
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ExpressionMarkdownMonoBlock() { // pre
|
|
||||||
auto separators = ExpressionSeparators(qsl("\\*/"));
|
|
||||||
return qsl("(^|[") + separators + qsl("])(````?)[\\s\\S]+?(````?)([") + separators + qsl("]|$)");
|
|
||||||
}
|
|
||||||
|
|
||||||
QRegularExpression CreateRegExp(const QString &expression) {
|
QRegularExpression CreateRegExp(const QString &expression) {
|
||||||
return QRegularExpression(expression, QRegularExpression::UseUnicodePropertiesOption);
|
auto result = QRegularExpression(
|
||||||
|
expression,
|
||||||
|
QRegularExpression::UseUnicodePropertiesOption);
|
||||||
|
result.optimize();
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
QSet<int32> CreateValidProtocols() {
|
QSet<int32> CreateValidProtocols() {
|
||||||
|
@ -1160,24 +1171,36 @@ const QRegularExpression &RegExpBotCommand() {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QRegularExpression &RegExpMarkdownBold() {
|
QString MarkdownBoldGoodBefore() {
|
||||||
static const auto result = CreateRegExp(ExpressionMarkdownBold());
|
return SeparatorsBold();
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const QRegularExpression &RegExpMarkdownItalic() {
|
QString MarkdownBoldBadAfter() {
|
||||||
static const auto result = CreateRegExp(ExpressionMarkdownItalic());
|
return qsl("*");
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const QRegularExpression &RegExpMarkdownMonoInline() {
|
QString MarkdownItalicGoodBefore() {
|
||||||
static const auto result = CreateRegExp(ExpressionMarkdownMonoInline());
|
return SeparatorsItalic();
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const QRegularExpression &RegExpMarkdownMonoBlock() {
|
QString MarkdownItalicBadAfter() {
|
||||||
static const auto result = CreateRegExp(ExpressionMarkdownMonoBlock());
|
return qsl("_");
|
||||||
return result;
|
}
|
||||||
|
|
||||||
|
QString MarkdownCodeGoodBefore() {
|
||||||
|
return SeparatorsMono();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString MarkdownCodeBadAfter() {
|
||||||
|
return qsl("`\n\r");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString MarkdownPreGoodBefore() {
|
||||||
|
return SeparatorsMono();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString MarkdownPreBadAfter() {
|
||||||
|
return qsl("`");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsValidProtocol(const QString &protocol) {
|
bool IsValidProtocol(const QString &protocol) {
|
||||||
|
@ -1546,252 +1569,6 @@ MTPVector<MTPMessageEntity> EntitiesToMTP(const EntitiesInText &entities, Conver
|
||||||
return MTP_vector<MTPMessageEntity>(std::move(v));
|
return MTP_vector<MTPMessageEntity>(std::move(v));
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MarkdownPart {
|
|
||||||
MarkdownPart() = default;
|
|
||||||
MarkdownPart(EntityInTextType type) : type(type), outerStart(-1) {
|
|
||||||
}
|
|
||||||
EntityInTextType type = EntityInTextInvalid;
|
|
||||||
int outerStart = 0;
|
|
||||||
int innerStart = 0;
|
|
||||||
int innerEnd = 0;
|
|
||||||
int outerEnd = 0;
|
|
||||||
bool addNewlineBefore = false;
|
|
||||||
bool addNewlineAfter = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
MarkdownPart GetMarkdownPart(EntityInTextType type, const QString &text, int matchFromOffset, bool rich) {
|
|
||||||
auto result = MarkdownPart();
|
|
||||||
auto regexp = [type] {
|
|
||||||
switch (type) {
|
|
||||||
case EntityInTextBold: return RegExpMarkdownBold();
|
|
||||||
case EntityInTextItalic: return RegExpMarkdownItalic();
|
|
||||||
case EntityInTextCode: return RegExpMarkdownMonoInline();
|
|
||||||
case EntityInTextPre: return RegExpMarkdownMonoBlock();
|
|
||||||
}
|
|
||||||
Unexpected("Type in GetMardownPart()");
|
|
||||||
};
|
|
||||||
|
|
||||||
if (matchFromOffset > 1) {
|
|
||||||
// If matchFromOffset is after some separator that is allowed to
|
|
||||||
// start our markdown tag the tag itself will start where we want it.
|
|
||||||
// So we allow to see this separator and make a match.
|
|
||||||
--matchFromOffset;
|
|
||||||
}
|
|
||||||
auto match = regexp().match(text, matchFromOffset);
|
|
||||||
if (!match.hasMatch()) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.outerStart = match.capturedStart();
|
|
||||||
result.outerEnd = match.capturedEnd();
|
|
||||||
if (!match.capturedRef(1).isEmpty()) {
|
|
||||||
++result.outerStart;
|
|
||||||
}
|
|
||||||
if (!match.capturedRef(4).isEmpty()) {
|
|
||||||
--result.outerEnd;
|
|
||||||
}
|
|
||||||
result.innerStart = result.outerStart + match.capturedLength(2);
|
|
||||||
result.innerEnd = result.outerEnd - match.capturedLength(3);
|
|
||||||
result.type = type;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AdjustMarkdownPrePart(MarkdownPart &result, const TextWithEntities &text, bool rich) {
|
|
||||||
auto start = text.text.constData();
|
|
||||||
auto length = text.text.size();
|
|
||||||
auto lastEntityBeforeEnd = 0;
|
|
||||||
auto firstEntityInsideStart = result.innerEnd;
|
|
||||||
auto lastEntityInsideEnd = result.innerStart;
|
|
||||||
auto firstEntityAfterStart = length;
|
|
||||||
for_const (auto &entity, text.entities) {
|
|
||||||
if (entity.offset() < result.outerStart) {
|
|
||||||
lastEntityBeforeEnd = entity.offset() + entity.length();
|
|
||||||
} else if (entity.offset() >= result.outerEnd) {
|
|
||||||
firstEntityAfterStart = entity.offset();
|
|
||||||
break;
|
|
||||||
} else if (entity.offset() >= result.innerStart) {
|
|
||||||
accumulate_min(firstEntityInsideStart, entity.offset());
|
|
||||||
lastEntityInsideEnd = entity.offset() + entity.length();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (result.outerStart > lastEntityBeforeEnd
|
|
||||||
&& chIsSpace(*(start + result.outerStart - 1), rich)
|
|
||||||
&& !chIsNewline(*(start + result.outerStart - 1))) {
|
|
||||||
--result.outerStart;
|
|
||||||
}
|
|
||||||
result.addNewlineBefore = (result.outerStart > 0 && !chIsNewline(*(start + result.outerStart - 1)));
|
|
||||||
|
|
||||||
for (auto testInnerStart = result.innerStart; testInnerStart < firstEntityInsideStart; ++testInnerStart) {
|
|
||||||
if (chIsNewline(*(start + testInnerStart))) {
|
|
||||||
result.innerStart = testInnerStart + 1;
|
|
||||||
break;
|
|
||||||
} else if (!chIsSpace(*(start + testInnerStart))) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (auto testInnerEnd = result.innerEnd; lastEntityInsideEnd < testInnerEnd;) {
|
|
||||||
--testInnerEnd;
|
|
||||||
if (chIsNewline(*(start + testInnerEnd))) {
|
|
||||||
result.innerEnd = testInnerEnd;
|
|
||||||
break;
|
|
||||||
} else if (!chIsSpace(*(start + testInnerEnd))) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (result.outerEnd < firstEntityAfterStart
|
|
||||||
&& chIsSpace(*(start + result.outerEnd))
|
|
||||||
&& !chIsNewline(*(start + result.outerEnd))) {
|
|
||||||
++result.outerEnd;
|
|
||||||
}
|
|
||||||
result.addNewlineAfter = (result.outerEnd < length && !chIsNewline(*(start + result.outerEnd)));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ParseMarkdown(
|
|
||||||
TextWithEntities &result,
|
|
||||||
const EntitiesInText &linkEntities,
|
|
||||||
bool rich) {
|
|
||||||
if (result.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto newResult = TextWithEntities();
|
|
||||||
|
|
||||||
MarkdownPart computedParts[4] = {
|
|
||||||
{ EntityInTextBold },
|
|
||||||
{ EntityInTextItalic },
|
|
||||||
{ EntityInTextPre },
|
|
||||||
{ EntityInTextCode },
|
|
||||||
};
|
|
||||||
|
|
||||||
auto existingEntityIndex = 0;
|
|
||||||
auto existingEntitiesCount = result.entities.size();
|
|
||||||
auto existingEntityShiftLeft = 0;
|
|
||||||
|
|
||||||
auto copyFromOffset = 0;
|
|
||||||
auto matchFromOffset = 0;
|
|
||||||
auto length = result.text.size();
|
|
||||||
auto nextCommandOffset = rich ? 0 : length;
|
|
||||||
auto inLink = false;
|
|
||||||
auto commandIsLink = false;
|
|
||||||
const auto start = result.text.constData();
|
|
||||||
for (; matchFromOffset < length;) {
|
|
||||||
if (nextCommandOffset <= matchFromOffset) {
|
|
||||||
for (nextCommandOffset = matchFromOffset; nextCommandOffset != length; ++nextCommandOffset) {
|
|
||||||
if (*(start + nextCommandOffset) == TextCommand) {
|
|
||||||
inLink = commandIsLink;
|
|
||||||
commandIsLink = textcmdStartsLink(start, length, nextCommandOffset);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (nextCommandOffset >= length) {
|
|
||||||
inLink = commandIsLink;
|
|
||||||
commandIsLink = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auto part = MarkdownPart();
|
|
||||||
auto checkType = [&part, &result, matchFromOffset, rich](MarkdownPart &computedPart) {
|
|
||||||
if (computedPart.type == EntityInTextInvalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (matchFromOffset > computedPart.outerStart) {
|
|
||||||
computedPart = GetMarkdownPart(computedPart.type, result.text, matchFromOffset, rich);
|
|
||||||
if (computedPart.type == EntityInTextInvalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (part.type == EntityInTextInvalid || part.outerStart > computedPart.outerStart) {
|
|
||||||
part = computedPart;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
for (auto &computedPart : computedParts) {
|
|
||||||
checkType(computedPart);
|
|
||||||
}
|
|
||||||
if (part.type == EntityInTextInvalid) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if start sequence intersects a command.
|
|
||||||
auto inCommand = checkTagStartInCommand(
|
|
||||||
start,
|
|
||||||
length,
|
|
||||||
part.outerStart,
|
|
||||||
nextCommandOffset,
|
|
||||||
commandIsLink,
|
|
||||||
inLink);
|
|
||||||
if (inCommand || inLink) {
|
|
||||||
matchFromOffset = nextCommandOffset;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if start or end sequences intersect any existing entity.
|
|
||||||
auto intersectedEntityEnd = 0;
|
|
||||||
for_const (auto &entity, result.entities) {
|
|
||||||
if (qMin(part.innerStart, entity.offset() + entity.length()) > qMax(part.outerStart, entity.offset()) ||
|
|
||||||
qMin(part.outerEnd, entity.offset() + entity.length()) > qMax(part.innerEnd, entity.offset())) {
|
|
||||||
intersectedEntityEnd = entity.offset() + entity.length();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if any of sequence outer edges are inside a link.
|
|
||||||
for_const (auto &entity, linkEntities) {
|
|
||||||
const auto startIntersects = (part.outerStart >= entity.offset())
|
|
||||||
&& (part.outerStart < entity.offset() + entity.length());
|
|
||||||
const auto endIntersects = (part.outerEnd > entity.offset())
|
|
||||||
&& (part.outerEnd <= entity.offset() + entity.length());
|
|
||||||
if (startIntersects || endIntersects) {
|
|
||||||
intersectedEntityEnd = entity.offset() + entity.length();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (intersectedEntityEnd > 0) {
|
|
||||||
matchFromOffset = qMax(part.innerStart, intersectedEntityEnd);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.type == EntityInTextPre) {
|
|
||||||
AdjustMarkdownPrePart(part, result, rich);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newResult.text.isEmpty()) newResult.text.reserve(result.text.size());
|
|
||||||
for (; existingEntityIndex < existingEntitiesCount && result.entities[existingEntityIndex].offset() < part.innerStart; ++existingEntityIndex) {
|
|
||||||
auto &entity = result.entities[existingEntityIndex];
|
|
||||||
newResult.entities.push_back(entity);
|
|
||||||
newResult.entities.back().shiftLeft(existingEntityShiftLeft);
|
|
||||||
}
|
|
||||||
if (part.outerStart > copyFromOffset) {
|
|
||||||
newResult.text.append(start + copyFromOffset, part.outerStart - copyFromOffset);
|
|
||||||
}
|
|
||||||
if (part.addNewlineBefore) newResult.text.append('\n');
|
|
||||||
existingEntityShiftLeft += (part.innerStart - part.outerStart) - (part.addNewlineBefore ? 1 : 0);
|
|
||||||
|
|
||||||
auto entityStart = newResult.text.size();
|
|
||||||
auto entityLength = part.innerEnd - part.innerStart;
|
|
||||||
newResult.entities.push_back(EntityInText(part.type, entityStart, entityLength));
|
|
||||||
|
|
||||||
for (; existingEntityIndex < existingEntitiesCount && result.entities[existingEntityIndex].offset() <= part.innerEnd; ++existingEntityIndex) {
|
|
||||||
auto &entity = result.entities[existingEntityIndex];
|
|
||||||
newResult.entities.push_back(entity);
|
|
||||||
newResult.entities.back().shiftLeft(existingEntityShiftLeft);
|
|
||||||
}
|
|
||||||
newResult.text.append(start + part.innerStart, entityLength);
|
|
||||||
if (part.addNewlineAfter) newResult.text.append('\n');
|
|
||||||
existingEntityShiftLeft += (part.outerEnd - part.innerEnd) - (part.addNewlineAfter ? 1 : 0);
|
|
||||||
|
|
||||||
copyFromOffset = matchFromOffset = part.outerEnd;
|
|
||||||
}
|
|
||||||
if (!newResult.empty()) {
|
|
||||||
newResult.text.append(start + copyFromOffset, length - copyFromOffset);
|
|
||||||
for (; existingEntityIndex < existingEntitiesCount; ++existingEntityIndex) {
|
|
||||||
auto &entity = result.entities[existingEntityIndex];
|
|
||||||
newResult.entities.push_back(entity);
|
|
||||||
newResult.entities.back().shiftLeft(existingEntityShiftLeft);
|
|
||||||
}
|
|
||||||
result = std::move(newResult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextWithEntities ParseEntities(const QString &text, int32 flags) {
|
TextWithEntities ParseEntities(const QString &text, int32 flags) {
|
||||||
const auto rich = ((flags & TextParseRichText) != 0);
|
const auto rich = ((flags & TextParseRichText) != 0);
|
||||||
auto result = TextWithEntities{ text, EntitiesInText() };
|
auto result = TextWithEntities{ text, EntitiesInText() };
|
||||||
|
@ -1801,12 +1578,6 @@ TextWithEntities ParseEntities(const QString &text, int32 flags) {
|
||||||
|
|
||||||
// Some code is duplicated in flattextarea.cpp!
|
// Some code is duplicated in flattextarea.cpp!
|
||||||
void ParseEntities(TextWithEntities &result, int32 flags, bool rich) {
|
void ParseEntities(TextWithEntities &result, int32 flags, bool rich) {
|
||||||
if (flags & TextParseMarkdown) { // parse markdown entities (bold, italic, code and pre)
|
|
||||||
auto copy = TextWithEntities{ result.text, EntitiesInText() };
|
|
||||||
ParseEntities(copy, TextParseLinks, false);
|
|
||||||
ParseMarkdown(result, copy.entities, rich);
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr auto kNotFound = std::numeric_limits<int>::max();
|
constexpr auto kNotFound = std::numeric_limits<int>::max();
|
||||||
|
|
||||||
auto newEntities = EntitiesInText();
|
auto newEntities = EntitiesInText();
|
||||||
|
@ -2059,74 +1830,6 @@ void ParseEntities(TextWithEntities &result, int32 flags, bool rich) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ApplyEntities(const TextWithEntities &text) {
|
|
||||||
if (text.entities.isEmpty()) return text.text;
|
|
||||||
|
|
||||||
QMultiMap<int32, QString> closingTags;
|
|
||||||
QMap<EntityInTextType, QString> tags;
|
|
||||||
tags.insert(EntityInTextCode, qsl("`"));
|
|
||||||
tags.insert(EntityInTextPre, qsl("```"));
|
|
||||||
tags.insert(EntityInTextBold, qsl("**"));
|
|
||||||
tags.insert(EntityInTextItalic, qsl("__"));
|
|
||||||
constexpr auto kLargestOpenCloseLength = 6;
|
|
||||||
|
|
||||||
QString result;
|
|
||||||
int32 size = text.text.size();
|
|
||||||
const QChar *b = text.text.constData(), *already = b, *e = b + size;
|
|
||||||
auto entity = text.entities.cbegin(), end = text.entities.cend();
|
|
||||||
auto skipTillRelevantAndGetTag = [&entity, &end, size, &tags] {
|
|
||||||
while (entity != end) {
|
|
||||||
if (entity->length() <= 0 || entity->offset() >= size) {
|
|
||||||
++entity;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
auto it = tags.constFind(entity->type());
|
|
||||||
if (it == tags.cend()) {
|
|
||||||
++entity;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return it.value();
|
|
||||||
}
|
|
||||||
return QString();
|
|
||||||
};
|
|
||||||
|
|
||||||
auto tag = skipTillRelevantAndGetTag();
|
|
||||||
while (entity != end || !closingTags.isEmpty()) {
|
|
||||||
auto nextOpenEntity = (entity == end) ? (size + 1) : entity->offset();
|
|
||||||
auto nextCloseEntity = closingTags.isEmpty() ? (size + 1) : closingTags.cbegin().key();
|
|
||||||
if (nextOpenEntity <= nextCloseEntity) {
|
|
||||||
if (result.isEmpty()) result.reserve(text.text.size() + text.entities.size() * kLargestOpenCloseLength);
|
|
||||||
|
|
||||||
const QChar *offset = b + nextOpenEntity;
|
|
||||||
if (offset > already) {
|
|
||||||
result.append(already, offset - already);
|
|
||||||
already = offset;
|
|
||||||
}
|
|
||||||
result.append(tag);
|
|
||||||
closingTags.insert(qMin(entity->offset() + entity->length(), size), tag);
|
|
||||||
|
|
||||||
++entity;
|
|
||||||
tag = skipTillRelevantAndGetTag();
|
|
||||||
} else {
|
|
||||||
const QChar *offset = b + nextCloseEntity;
|
|
||||||
if (offset > already) {
|
|
||||||
result.append(already, offset - already);
|
|
||||||
already = offset;
|
|
||||||
}
|
|
||||||
result.append(closingTags.cbegin().value());
|
|
||||||
closingTags.erase(closingTags.begin());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (result.isEmpty()) {
|
|
||||||
return text.text;
|
|
||||||
}
|
|
||||||
const QChar *offset = b + size;
|
|
||||||
if (offset > already) {
|
|
||||||
result.append(already, offset - already);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MoveStringPart(TextWithEntities &result, int to, int from, int count) {
|
void MoveStringPart(TextWithEntities &result, int to, int from, int count) {
|
||||||
if (!count) return;
|
if (!count) return;
|
||||||
if (to != from) {
|
if (to != from) {
|
||||||
|
|
|
@ -163,10 +163,14 @@ const QRegularExpression &RegExpHashtag();
|
||||||
const QRegularExpression &RegExpHashtagExclude();
|
const QRegularExpression &RegExpHashtagExclude();
|
||||||
const QRegularExpression &RegExpMention();
|
const QRegularExpression &RegExpMention();
|
||||||
const QRegularExpression &RegExpBotCommand();
|
const QRegularExpression &RegExpBotCommand();
|
||||||
const QRegularExpression &RegExpMarkdownBold();
|
QString MarkdownBoldGoodBefore();
|
||||||
const QRegularExpression &RegExpMarkdownItalic();
|
QString MarkdownBoldBadAfter();
|
||||||
const QRegularExpression &RegExpMarkdownMonoInline();
|
QString MarkdownItalicGoodBefore();
|
||||||
const QRegularExpression &RegExpMarkdownMonoBlock();
|
QString MarkdownItalicBadAfter();
|
||||||
|
QString MarkdownCodeGoodBefore();
|
||||||
|
QString MarkdownCodeBadAfter();
|
||||||
|
QString MarkdownPreGoodBefore();
|
||||||
|
QString MarkdownPreBadAfter();
|
||||||
|
|
||||||
inline void Append(TextWithEntities &to, TextWithEntities &&append) {
|
inline void Append(TextWithEntities &to, TextWithEntities &&append) {
|
||||||
auto entitiesShiftRight = to.text.size();
|
auto entitiesShiftRight = to.text.size();
|
||||||
|
@ -218,7 +222,6 @@ MTPVector<MTPMessageEntity> EntitiesToMTP(const EntitiesInText &entities, Conver
|
||||||
// Changes text if (flags & TextParseMarkdown).
|
// Changes text if (flags & TextParseMarkdown).
|
||||||
TextWithEntities ParseEntities(const QString &text, int32 flags);
|
TextWithEntities ParseEntities(const QString &text, int32 flags);
|
||||||
void ParseEntities(TextWithEntities &result, int32 flags, bool rich = false);
|
void ParseEntities(TextWithEntities &result, int32 flags, bool rich = false);
|
||||||
QString ApplyEntities(const TextWithEntities &text);
|
|
||||||
|
|
||||||
void PrepareForSending(TextWithEntities &result, int32 flags);
|
void PrepareForSending(TextWithEntities &result, int32 flags);
|
||||||
void Trim(TextWithEntities &result);
|
void Trim(TextWithEntities &result);
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -122,6 +122,16 @@ public:
|
||||||
};
|
};
|
||||||
using TagList = TextWithTags::Tags;
|
using TagList = TextWithTags::Tags;
|
||||||
|
|
||||||
|
struct PossibleTag {
|
||||||
|
int start = 0;
|
||||||
|
int length = 0;
|
||||||
|
QString tag;
|
||||||
|
};
|
||||||
|
static const QString kTagBold;
|
||||||
|
static const QString kTagItalic;
|
||||||
|
static const QString kTagCode;
|
||||||
|
static const QString kTagPre;
|
||||||
|
|
||||||
InputField(
|
InputField(
|
||||||
QWidget *parent,
|
QWidget *parent,
|
||||||
const style::InputField &st,
|
const style::InputField &st,
|
||||||
|
@ -174,6 +184,8 @@ public:
|
||||||
};
|
};
|
||||||
void setTagMimeProcessor(std::unique_ptr<TagMimeProcessor> &&processor);
|
void setTagMimeProcessor(std::unique_ptr<TagMimeProcessor> &&processor);
|
||||||
|
|
||||||
|
void setAdditionalMargin(int margin);
|
||||||
|
|
||||||
void setInstantReplaces(const InstantReplaces &replaces);
|
void setInstantReplaces(const InstantReplaces &replaces);
|
||||||
void enableInstantReplaces(bool enabled);
|
void enableInstantReplaces(bool enabled);
|
||||||
void commitInstantReplacement(
|
void commitInstantReplacement(
|
||||||
|
@ -181,6 +193,11 @@ public:
|
||||||
int till,
|
int till,
|
||||||
const QString &with,
|
const QString &with,
|
||||||
base::optional<QString> checkOriginal = base::none);
|
base::optional<QString> checkOriginal = base::none);
|
||||||
|
bool commitMarkdownReplacement(
|
||||||
|
int from,
|
||||||
|
int till,
|
||||||
|
const QString &tag,
|
||||||
|
const QString &edge = QString());
|
||||||
|
|
||||||
const QString &getLastText() const {
|
const QString &getLastText() const {
|
||||||
return _lastTextWithTags.text;
|
return _lastTextWithTags.text;
|
||||||
|
@ -212,6 +229,7 @@ public:
|
||||||
Both,
|
Both,
|
||||||
};
|
};
|
||||||
void setSubmitSettings(SubmitSettings settings);
|
void setSubmitSettings(SubmitSettings settings);
|
||||||
|
void enableMarkdownSupport(bool enabled = true);
|
||||||
void customUpDown(bool isCustom);
|
void customUpDown(bool isCustom);
|
||||||
|
|
||||||
not_null<QTextDocument*> document();
|
not_null<QTextDocument*> document();
|
||||||
|
@ -246,7 +264,6 @@ private slots:
|
||||||
void onTouchTimer();
|
void onTouchTimer();
|
||||||
|
|
||||||
void onDocumentContentsChange(int position, int charsRemoved, int charsAdded);
|
void onDocumentContentsChange(int position, int charsRemoved, int charsAdded);
|
||||||
void onDocumentContentsChanged();
|
|
||||||
|
|
||||||
void onUndoAvailable(bool avail);
|
void onUndoAvailable(bool avail);
|
||||||
void onRedoAvailable(bool avail);
|
void onRedoAvailable(bool avail);
|
||||||
|
@ -276,6 +293,7 @@ private:
|
||||||
class Inner;
|
class Inner;
|
||||||
friend class Inner;
|
friend class Inner;
|
||||||
|
|
||||||
|
void handleContentsChanged();
|
||||||
bool viewportEventInner(QEvent *e);
|
bool viewportEventInner(QEvent *e);
|
||||||
QVariant loadResource(int type, const QUrl &name);
|
QVariant loadResource(int type, const QUrl &name);
|
||||||
void handleTouchEvent(QTouchEvent *e);
|
void handleTouchEvent(QTouchEvent *e);
|
||||||
|
@ -305,7 +323,8 @@ private:
|
||||||
int start,
|
int start,
|
||||||
int end,
|
int end,
|
||||||
TagList &outTagsList,
|
TagList &outTagsList,
|
||||||
bool &outTagsChanged) const;
|
bool &outTagsChanged,
|
||||||
|
std::vector<PossibleTag> *outPossibleTags = nullptr) const;
|
||||||
|
|
||||||
// After any characters added we must postprocess them. This includes:
|
// After any characters added we must postprocess them. This includes:
|
||||||
// 1. Replacing font family to semibold for ~ characters, if we used Open Sans 13px.
|
// 1. Replacing font family to semibold for ~ characters, if we used Open Sans 13px.
|
||||||
|
@ -318,12 +337,16 @@ private:
|
||||||
|
|
||||||
void chopByMaxLength(int insertPosition, int insertLength);
|
void chopByMaxLength(int insertPosition, int insertLength);
|
||||||
|
|
||||||
|
bool processMarkdownReplaces(const QString &appended);
|
||||||
|
bool processMarkdownReplace(const QString &tag);
|
||||||
|
|
||||||
// We don't want accidentally detach InstantReplaces map.
|
// We don't want accidentally detach InstantReplaces map.
|
||||||
// So we access it only by const reference from this method.
|
// So we access it only by const reference from this method.
|
||||||
const InstantReplaces &instantReplaces() const;
|
const InstantReplaces &instantReplaces() const;
|
||||||
void processInstantReplaces(const QString &text);
|
void processInstantReplaces(const QString &appended);
|
||||||
void applyInstantReplace(const QString &what, const QString &with);
|
void applyInstantReplace(const QString &what, const QString &with);
|
||||||
bool revertInstantReplace();
|
|
||||||
|
bool revertFormatReplace();
|
||||||
|
|
||||||
const style::InputField &_st;
|
const style::InputField &_st;
|
||||||
|
|
||||||
|
@ -336,6 +359,7 @@ private:
|
||||||
object_ptr<Inner> _inner;
|
object_ptr<Inner> _inner;
|
||||||
|
|
||||||
TextWithTags _lastTextWithTags;
|
TextWithTags _lastTextWithTags;
|
||||||
|
std::vector<PossibleTag> _textAreaPossibleTags;
|
||||||
|
|
||||||
// Tags list which we should apply while setText() call or insert from mime data.
|
// Tags list which we should apply while setText() call or insert from mime data.
|
||||||
TagList _insertedTags;
|
TagList _insertedTags;
|
||||||
|
@ -349,11 +373,12 @@ private:
|
||||||
std::unique_ptr<TagMimeProcessor> _tagMimeProcessor;
|
std::unique_ptr<TagMimeProcessor> _tagMimeProcessor;
|
||||||
|
|
||||||
SubmitSettings _submitSettings = SubmitSettings::Enter;
|
SubmitSettings _submitSettings = SubmitSettings::Enter;
|
||||||
|
bool _markdownEnabled = false;
|
||||||
bool _undoAvailable = false;
|
bool _undoAvailable = false;
|
||||||
bool _redoAvailable = false;
|
bool _redoAvailable = false;
|
||||||
bool _inDrop = false;
|
bool _inDrop = false;
|
||||||
bool _inHeightCheck = false;
|
bool _inHeightCheck = false;
|
||||||
int _fakeMargin = 0;
|
int _additionalMargin = 0;
|
||||||
|
|
||||||
bool _customUpDown = false;
|
bool _customUpDown = false;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue