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());
|
||||
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 (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;
|
||||
|
@ -73,12 +87,21 @@ TextWithTags::Tags ConvertEntitiesToTextTags(const EntitiesInText &entities) {
|
|||
}
|
||||
|
||||
result.reserve(entities.size());
|
||||
for_const (auto &entity, entities) {
|
||||
if (entity.type() == EntityInTextMentionName) {
|
||||
for (const auto &entity : entities) {
|
||||
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());
|
||||
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;
|
||||
|
@ -119,15 +142,16 @@ void InitMessageField(not_null<Ui::InputField*> field) {
|
|||
field->setTagMimeProcessor(std::make_unique<FieldTagMimeProcessor>());
|
||||
|
||||
field->document()->setDocumentMargin(4.);
|
||||
const auto additional = convertScale(4) - 4;
|
||||
field->rawTextEdit()->setStyleSheet(
|
||||
qsl("QTextEdit { margin: %1px; }").arg(additional));
|
||||
field->setAdditionalMargin(convertScale(4) - 4);
|
||||
|
||||
field->customTab(true);
|
||||
field->setInstantReplaces(Ui::InstantReplaces::Default());
|
||||
field->enableInstantReplaces(Global::ReplaceEmoji());
|
||||
field->enableMarkdownSupport(Global::ReplaceEmoji());
|
||||
auto &changed = Global::RefReplaceEmojiChanged();
|
||||
Ui::AttachAsChild(field, changed.add_subscription([=] {
|
||||
field->enableInstantReplaces(Global::ReplaceEmoji());
|
||||
field->enableMarkdownSupport(Global::ReplaceEmoji());
|
||||
}));
|
||||
field->window()->activateWindow();
|
||||
}
|
||||
|
|
|
@ -45,8 +45,13 @@ Draft::Draft(
|
|||
|
||||
void applyPeerCloudDraft(PeerId peerId, const MTPDdraftMessage &draft) {
|
||||
auto history = App::history(peerId);
|
||||
auto text = TextWithEntities { qs(draft.vmessage), draft.has_entities() ? TextUtilities::EntitiesFromMTP(draft.ventities.v) : EntitiesInText() };
|
||||
auto textWithTags = TextWithTags { TextUtilities::ApplyEntities(text), ConvertEntitiesToTextTags(text.entities) };
|
||||
auto textWithTags = TextWithTags {
|
||||
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 cloudDraft = std::make_unique<Draft>(textWithTags, replyTo, MessageCursor(QFIXED_MAX, QFIXED_MAX, QFIXED_MAX), draft.is_no_webpage());
|
||||
cloudDraft->date = draft.vdate.v;
|
||||
|
|
|
@ -5836,7 +5836,7 @@ void HistoryWidget::editMessage(not_null<HistoryItem*> item) {
|
|||
|
||||
const auto original = item->originalText();
|
||||
const auto editData = TextWithTags {
|
||||
TextUtilities::ApplyEntities(original),
|
||||
original.text,
|
||||
ConvertEntitiesToTextTags(original.entities)
|
||||
};
|
||||
const auto cursor = MessageCursor {
|
||||
|
@ -6235,7 +6235,10 @@ void HistoryWidget::onCancel() {
|
|||
onInlineBotCancel();
|
||||
} else if (_editMsgId) {
|
||||
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()) {
|
||||
Ui::show(Box<ConfirmBox>(
|
||||
lang(lng_cancel_edit_post_sure),
|
||||
|
|
|
@ -234,7 +234,9 @@ public:
|
|||
if (flags & flag) {
|
||||
createBlock();
|
||||
flags &= ~flag;
|
||||
if (flag == TextBlockFPre) {
|
||||
if (flag == TextBlockFPre
|
||||
&& !_t->_blocks.empty()
|
||||
&& _t->_blocks.back()->type() != TextBlockTNewline) {
|
||||
newlineAwaited = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,12 +30,39 @@ QString ExpressionMailNameAtEnd() {
|
|||
return qsl("[a-zA-Z\\-_\\.0-9]{1,256}$");
|
||||
}
|
||||
|
||||
QString ExpressionSeparators(const QString &additional) {
|
||||
QString Quotes() {
|
||||
// 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;
|
||||
}
|
||||
|
||||
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() {
|
||||
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]|$)");
|
||||
}
|
||||
|
||||
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) {
|
||||
return QRegularExpression(expression, QRegularExpression::UseUnicodePropertiesOption);
|
||||
auto result = QRegularExpression(
|
||||
expression,
|
||||
QRegularExpression::UseUnicodePropertiesOption);
|
||||
result.optimize();
|
||||
return result;
|
||||
}
|
||||
|
||||
QSet<int32> CreateValidProtocols() {
|
||||
|
@ -1160,24 +1171,36 @@ const QRegularExpression &RegExpBotCommand() {
|
|||
return result;
|
||||
}
|
||||
|
||||
const QRegularExpression &RegExpMarkdownBold() {
|
||||
static const auto result = CreateRegExp(ExpressionMarkdownBold());
|
||||
return result;
|
||||
QString MarkdownBoldGoodBefore() {
|
||||
return SeparatorsBold();
|
||||
}
|
||||
|
||||
const QRegularExpression &RegExpMarkdownItalic() {
|
||||
static const auto result = CreateRegExp(ExpressionMarkdownItalic());
|
||||
return result;
|
||||
QString MarkdownBoldBadAfter() {
|
||||
return qsl("*");
|
||||
}
|
||||
|
||||
const QRegularExpression &RegExpMarkdownMonoInline() {
|
||||
static const auto result = CreateRegExp(ExpressionMarkdownMonoInline());
|
||||
return result;
|
||||
QString MarkdownItalicGoodBefore() {
|
||||
return SeparatorsItalic();
|
||||
}
|
||||
|
||||
const QRegularExpression &RegExpMarkdownMonoBlock() {
|
||||
static const auto result = CreateRegExp(ExpressionMarkdownMonoBlock());
|
||||
return result;
|
||||
QString MarkdownItalicBadAfter() {
|
||||
return qsl("_");
|
||||
}
|
||||
|
||||
QString MarkdownCodeGoodBefore() {
|
||||
return SeparatorsMono();
|
||||
}
|
||||
|
||||
QString MarkdownCodeBadAfter() {
|
||||
return qsl("`\n\r");
|
||||
}
|
||||
|
||||
QString MarkdownPreGoodBefore() {
|
||||
return SeparatorsMono();
|
||||
}
|
||||
|
||||
QString MarkdownPreBadAfter() {
|
||||
return qsl("`");
|
||||
}
|
||||
|
||||
bool IsValidProtocol(const QString &protocol) {
|
||||
|
@ -1546,252 +1569,6 @@ MTPVector<MTPMessageEntity> EntitiesToMTP(const EntitiesInText &entities, Conver
|
|||
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) {
|
||||
const auto rich = ((flags & TextParseRichText) != 0);
|
||||
auto result = TextWithEntities{ text, EntitiesInText() };
|
||||
|
@ -1801,12 +1578,6 @@ TextWithEntities ParseEntities(const QString &text, int32 flags) {
|
|||
|
||||
// Some code is duplicated in flattextarea.cpp!
|
||||
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();
|
||||
|
||||
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) {
|
||||
if (!count) return;
|
||||
if (to != from) {
|
||||
|
|
|
@ -163,10 +163,14 @@ const QRegularExpression &RegExpHashtag();
|
|||
const QRegularExpression &RegExpHashtagExclude();
|
||||
const QRegularExpression &RegExpMention();
|
||||
const QRegularExpression &RegExpBotCommand();
|
||||
const QRegularExpression &RegExpMarkdownBold();
|
||||
const QRegularExpression &RegExpMarkdownItalic();
|
||||
const QRegularExpression &RegExpMarkdownMonoInline();
|
||||
const QRegularExpression &RegExpMarkdownMonoBlock();
|
||||
QString MarkdownBoldGoodBefore();
|
||||
QString MarkdownBoldBadAfter();
|
||||
QString MarkdownItalicGoodBefore();
|
||||
QString MarkdownItalicBadAfter();
|
||||
QString MarkdownCodeGoodBefore();
|
||||
QString MarkdownCodeBadAfter();
|
||||
QString MarkdownPreGoodBefore();
|
||||
QString MarkdownPreBadAfter();
|
||||
|
||||
inline void Append(TextWithEntities &to, TextWithEntities &&append) {
|
||||
auto entitiesShiftRight = to.text.size();
|
||||
|
@ -218,7 +222,6 @@ MTPVector<MTPMessageEntity> EntitiesToMTP(const EntitiesInText &entities, Conver
|
|||
// Changes text if (flags & TextParseMarkdown).
|
||||
TextWithEntities ParseEntities(const QString &text, int32 flags);
|
||||
void ParseEntities(TextWithEntities &result, int32 flags, bool rich = false);
|
||||
QString ApplyEntities(const TextWithEntities &text);
|
||||
|
||||
void PrepareForSending(TextWithEntities &result, int32 flags);
|
||||
void Trim(TextWithEntities &result);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -122,6 +122,16 @@ public:
|
|||
};
|
||||
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(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
|
@ -174,6 +184,8 @@ public:
|
|||
};
|
||||
void setTagMimeProcessor(std::unique_ptr<TagMimeProcessor> &&processor);
|
||||
|
||||
void setAdditionalMargin(int margin);
|
||||
|
||||
void setInstantReplaces(const InstantReplaces &replaces);
|
||||
void enableInstantReplaces(bool enabled);
|
||||
void commitInstantReplacement(
|
||||
|
@ -181,6 +193,11 @@ public:
|
|||
int till,
|
||||
const QString &with,
|
||||
base::optional<QString> checkOriginal = base::none);
|
||||
bool commitMarkdownReplacement(
|
||||
int from,
|
||||
int till,
|
||||
const QString &tag,
|
||||
const QString &edge = QString());
|
||||
|
||||
const QString &getLastText() const {
|
||||
return _lastTextWithTags.text;
|
||||
|
@ -212,6 +229,7 @@ public:
|
|||
Both,
|
||||
};
|
||||
void setSubmitSettings(SubmitSettings settings);
|
||||
void enableMarkdownSupport(bool enabled = true);
|
||||
void customUpDown(bool isCustom);
|
||||
|
||||
not_null<QTextDocument*> document();
|
||||
|
@ -246,7 +264,6 @@ private slots:
|
|||
void onTouchTimer();
|
||||
|
||||
void onDocumentContentsChange(int position, int charsRemoved, int charsAdded);
|
||||
void onDocumentContentsChanged();
|
||||
|
||||
void onUndoAvailable(bool avail);
|
||||
void onRedoAvailable(bool avail);
|
||||
|
@ -276,6 +293,7 @@ private:
|
|||
class Inner;
|
||||
friend class Inner;
|
||||
|
||||
void handleContentsChanged();
|
||||
bool viewportEventInner(QEvent *e);
|
||||
QVariant loadResource(int type, const QUrl &name);
|
||||
void handleTouchEvent(QTouchEvent *e);
|
||||
|
@ -305,7 +323,8 @@ private:
|
|||
int start,
|
||||
int end,
|
||||
TagList &outTagsList,
|
||||
bool &outTagsChanged) const;
|
||||
bool &outTagsChanged,
|
||||
std::vector<PossibleTag> *outPossibleTags = nullptr) const;
|
||||
|
||||
// After any characters added we must postprocess them. This includes:
|
||||
// 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);
|
||||
|
||||
bool processMarkdownReplaces(const QString &appended);
|
||||
bool processMarkdownReplace(const QString &tag);
|
||||
|
||||
// We don't want accidentally detach InstantReplaces map.
|
||||
// So we access it only by const reference from this method.
|
||||
const InstantReplaces &instantReplaces() const;
|
||||
void processInstantReplaces(const QString &text);
|
||||
void processInstantReplaces(const QString &appended);
|
||||
void applyInstantReplace(const QString &what, const QString &with);
|
||||
bool revertInstantReplace();
|
||||
|
||||
bool revertFormatReplace();
|
||||
|
||||
const style::InputField &_st;
|
||||
|
||||
|
@ -336,6 +359,7 @@ private:
|
|||
object_ptr<Inner> _inner;
|
||||
|
||||
TextWithTags _lastTextWithTags;
|
||||
std::vector<PossibleTag> _textAreaPossibleTags;
|
||||
|
||||
// Tags list which we should apply while setText() call or insert from mime data.
|
||||
TagList _insertedTags;
|
||||
|
@ -349,11 +373,12 @@ private:
|
|||
std::unique_ptr<TagMimeProcessor> _tagMimeProcessor;
|
||||
|
||||
SubmitSettings _submitSettings = SubmitSettings::Enter;
|
||||
bool _markdownEnabled = false;
|
||||
bool _undoAvailable = false;
|
||||
bool _redoAvailable = false;
|
||||
bool _inDrop = false;
|
||||
bool _inHeightCheck = false;
|
||||
int _fakeMargin = 0;
|
||||
int _additionalMargin = 0;
|
||||
|
||||
bool _customUpDown = false;
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue