Support markdown replaces in Ui::InputField.

This commit is contained in:
John Preston 2018-05-22 22:09:13 +03:00
parent 017ec87d60
commit 6f6ec217e3
8 changed files with 902 additions and 544 deletions

View file

@ -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();
} }

View file

@ -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;

View file

@ -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),

View file

@ -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;
} }
} }

View file

@ -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) {

View file

@ -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

View file

@ -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;