diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index b83696c731..cce2b56a34 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -464,7 +464,11 @@ void GroupInfoBox::submit() { if (_creationRequestId) return; auto title = TextUtilities::PrepareForSending(_title->getLastText()); - auto description = _description ? TextUtilities::PrepareForSending(_description->getLastText(), TextUtilities::PrepareTextOption::CheckLinks) : QString(); + auto description = _description + ? TextUtilities::PrepareForSending( + _description->getLastText(), + TextUtilities::PrepareTextOption::CheckLinks) + : QString(); if (title.isEmpty()) { _title->setFocus(); _title->showError(); diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index fcadf78a29..3c8c2483a7 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -345,7 +345,7 @@ void EditCaptionBox::save() { if (_previewCancelled) { flags |= MTPmessages_EditMessage::Flag::f_no_webpage; } - const auto textWithTags = _field->getTextWithTags(); + const auto textWithTags = _field->getTextWithAppliedMarkdown(); auto sending = TextWithEntities{ textWithTags.text, ConvertTextTagsToEntities(textWithTags.tags) diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 0faaf81e4b..fbe11364a1 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -1795,7 +1795,7 @@ void SendFilesBox::send(bool ctrlShiftEnter) { _confirmed = true; if (_confirmedCallback) { auto caption = _caption - ? _caption->getTextWithTags() + ? _caption->getTextWithAppliedMarkdown() : TextWithTags(); _confirmedCallback( std::move(_list), diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index ede0fbfca7..2941f4de7a 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -571,6 +571,7 @@ void MessageLinksParser::parse() { const auto &textWithTags = _field->getTextWithTags(); const auto &text = textWithTags.text; const auto &tags = textWithTags.tags; + const auto &markdownTags = _field->getMarkdownTags(); if (text.isEmpty()) { _list = QStringList(); return; @@ -578,9 +579,8 @@ void MessageLinksParser::parse() { auto ranges = QVector(); - const auto tagsBegin = tags.begin(); + auto tag = tags.begin(); const auto tagsEnd = tags.end(); - auto tag = tagsBegin; const auto processTag = [&] { Expects(tag != tagsEnd); @@ -605,6 +605,25 @@ void MessageLinksParser::parse() { return true; }; + auto markdownTag = markdownTags.begin(); + const auto markdownTagsEnd = markdownTags.end(); + const auto markdownTagsAllow = [&](int from, int length) { + while (markdownTag != markdownTagsEnd + && (markdownTag->start + markdownTag->length <= from + || !markdownTag->closed)) { + ++markdownTag; + continue; + } + if (markdownTag == markdownTagsEnd + || markdownTag->start >= from + length) { + return true; + } + // Ignore http-links that are completely inside some tags. + // This will allow sending http://test.com/__test__/test correctly. + return (markdownTag->start > from + || markdownTag->start + markdownTag->length < from + length); + }; + const auto len = text.size(); const QChar *start = text.unicode(), *end = start + text.size(); for (auto offset = 0, matchOffset = offset; offset < len;) { @@ -671,7 +690,9 @@ void MessageLinksParser::parse() { }; processTagsBefore(domainOffset); if (!hasTagsIntersection(range.start + range.length)) { - ranges.push_back(range); + if (markdownTagsAllow(range.start, range.length)) { + ranges.push_back(range); + } } offset = matchOffset = p - start; } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 00a14372ab..e1210debc8 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -2891,10 +2891,14 @@ void HistoryWidget::showNextUnreadMention() { void HistoryWidget::saveEditMsg() { if (_saveEditMsgRequestId) return; - WebPageId webPageId = _previewCancelled ? CancelledWebPageId : ((_previewData && _previewData->pendingTill >= 0) ? _previewData->id : 0); + const auto webPageId = _previewCancelled + ? CancelledWebPageId + : ((_previewData && _previewData->pendingTill >= 0) + ? _previewData->id + : WebPageId(0)); - auto &textWithTags = _field->getTextWithTags(); - auto prepareFlags = Ui::ItemTextOptions(_history, App::self()).flags; + const auto textWithTags = _field->getTextWithAppliedMarkdown(); + const auto prepareFlags = Ui::ItemTextOptions(_history, App::self()).flags; auto sending = TextWithEntities(); auto left = TextWithEntities { textWithTags.text, ConvertTextTagsToEntities(textWithTags.tags) }; TextUtilities::PrepareForSending(left, prepareFlags); @@ -3000,7 +3004,7 @@ void HistoryWidget::send() { WebPageId webPageId = _previewCancelled ? CancelledWebPageId : ((_previewData && _previewData->pendingTill >= 0) ? _previewData->id : 0); auto message = MainWidget::MessageToSend(_history); - message.textWithTags = _field->getTextWithTags(); + message.textWithTags = _field->getTextWithAppliedMarkdown(); message.replyTo = replyToId(); message.webPageId = webPageId; App::main()->sendMessage(message); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index a5e2b52a36..9ad18e6067 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -1249,7 +1249,10 @@ void MainWidget::sendMessage(const MessageToSend &message) { saveRecentHashtags(textWithTags.text); auto sending = TextWithEntities(); - auto left = TextWithEntities { textWithTags.text, ConvertTextTagsToEntities(textWithTags.tags) }; + auto left = TextWithEntities { + textWithTags.text, + ConvertTextTagsToEntities(textWithTags.tags) + }; auto prepareFlags = Ui::ItemTextOptions(history, App::self()).flags; TextUtilities::PrepareForSending(left, prepareFlags); diff --git a/Telegram/SourceFiles/ui/text/text_entity.cpp b/Telegram/SourceFiles/ui/text/text_entity.cpp index cde20f100f..88279a4cbd 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.cpp +++ b/Telegram/SourceFiles/ui/text/text_entity.cpp @@ -1579,7 +1579,7 @@ TextWithEntities ParseEntities(const QString &text, int32 flags) { return result; } -// Some code is duplicated in flattextarea.cpp! +// Some code is duplicated in message_field.cpp! void ParseEntities(TextWithEntities &result, int32 flags, bool rich) { constexpr auto kNotFound = std::numeric_limits::max(); diff --git a/Telegram/SourceFiles/ui/text/text_entity.h b/Telegram/SourceFiles/ui/text/text_entity.h index bcfb08beb0..8761826dd1 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.h +++ b/Telegram/SourceFiles/ui/text/text_entity.h @@ -126,7 +126,8 @@ enum { struct TextWithTags { struct Tag { - int offset, length; + int offset = 0; + int length = 0; QString id; }; using Tags = QVector; diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.cpp b/Telegram/SourceFiles/ui/widgets/input_fields.cpp index efdba8c73d..f669ab9f58 100644 --- a/Telegram/SourceFiles/ui/widgets/input_fields.cpp +++ b/Telegram/SourceFiles/ui/widgets/input_fields.cpp @@ -128,17 +128,99 @@ struct TagStartExpression { QString badAfter; }; -struct TagStartItem { - int offset = 0; - int position = -1; -}; - constexpr auto kTagBoldIndex = 0; constexpr auto kTagItalicIndex = 1; constexpr auto kTagCodeIndex = 2; constexpr auto kTagPreIndex = 3; constexpr auto kInvalidPosition = std::numeric_limits::max() / 2; +class TagSearchItem { +public: + enum class Edge { + Open, + Close, + }; + + int matchPosition(Edge edge) const { + return (_position >= 0) ? _position : kInvalidPosition; + } + + void applyOffset(int offset) { + if (_position < offset) { + _position = -1; + } + accumulate_max(_offset, offset); + } + + void fill( + const QString &text, + Edge edge, + const TagStartExpression &expression) { + const auto length = text.size(); + const auto &tag = expression.tag; + const auto tagLength = tag.size(); + const auto isGood = [&](QChar ch) { + return (expression.goodBefore.indexOf(ch) >= 0); + }; + const auto isBad = [&](QChar ch) { + return (expression.badAfter.indexOf(ch) >= 0); + }; + const auto check = [&](Edge edge) { + if (_position > 0) { + const auto before = text[_position - 1]; + if ((edge == Edge::Open && !isGood(before)) + || (edge == Edge::Close && isBad(before))) { + return false; + } + } + if (_position + tagLength < length) { + const auto after = text[_position + tagLength]; + if ((edge == Edge::Open && isBad(after)) + || (edge == Edge::Close && !isGood(after))) { + return false; + } + } + return true; + }; + const auto edgeIndex = static_cast(edge); + if (_position >= 0) { + if (_checked[edgeIndex]) { + return; + } else if (check(edge)) { + _checked[edgeIndex] = true; + return; + } else { + _checked = { false, false }; + } + } + while (true) { + _position = text.indexOf(tag, _offset); + if (_position < 0) { + _offset = _position = kInvalidPosition; + break; + } + _offset = _position + tagLength; + if (check(edge)) { + break; + } else { + continue; + } + } + if (_position == kInvalidPosition) { + _checked = { true, true }; + } else { + _checked = { false, false }; + _checked[edgeIndex] = true; + } + } + +private: + int _offset = 0; + int _position = -1; + std::array _checked = { false, false }; + +}; + const std::vector &TagStartExpressions() { static auto cached = std::vector { { @@ -165,12 +247,12 @@ const std::vector &TagStartExpressions() { return cached; } -const std::map> &TagFinishIndices() { - static auto cached = std::map> { - { kTagBold, { kTagBoldIndex, kTagCodeIndex, kTagPreIndex } }, - { kTagItalic, { kTagItalicIndex, kTagCodeIndex, kTagPreIndex } }, - { kTagCode, { kTagCodeIndex, kTagPreIndex } }, - { kTagPre, { kTagPreIndex } }, +const std::map &TagIndices() { + static auto cached = std::map { + { kTagBold, kTagBoldIndex }, + { kTagItalic, kTagItalicIndex }, + { kTagCode, kTagCodeIndex }, + { kTagPre, kTagPreIndex }, }; return cached; } @@ -179,12 +261,14 @@ bool DoesTagFinishByNewline(const QString &tag) { return (tag == kTagCode); } -class PossibleTagAccumulator { +class MarkdownTagAccumulator { public: - PossibleTagAccumulator(std::vector *tags) + using Edge = TagSearchItem::Edge; + + MarkdownTagAccumulator(std::vector *tags) : _tags(tags) , _expressions(TagStartExpressions()) - , _finishIndices(TagFinishIndices()) + , _tagIndices(TagIndices()) , _items(_expressions.size()) { } @@ -200,41 +284,49 @@ public: return; } for (auto &item : _items) { - item = TagStartItem(); + item = TagSearchItem(); } - auto tagIndex = _currentTag; + auto tryFinishTag = _currentTag; while (true) { - for (; tagIndex != _currentFreeTag; ++tagIndex) { - auto &tag = (*_tags)[tagIndex]; - bumpOffsetByTag(tag, tag.start + 1); - - const auto finishIt = _finishIndices.find(tag.tag); - Assert(finishIt != end(_finishIndices)); - const auto &finishingIndices = finishIt->second; - for (const auto index : finishingIndices) { - fillItem(index, text); - } - if (finishByNewline(tagIndex, text, finishingIndices)) { + for (; tryFinishTag != _currentFreeTag; ++tryFinishTag) { + auto &tag = (*_tags)[tryFinishTag]; + if (tag.length >= 0) { continue; } - const auto min = minIndex(finishingIndices); - if (min >= 0) { - const auto minPosition = matchPosition(min); - finishTag(tagIndex, _currentLength + minPosition); - } else if (tag.tag == kTagPre || tag.tag == kTagCode) { - // We can't finish a mono tag, so we ignore all others. - return; + + const auto i = _tagIndices.find(tag.tag); + Assert(i != end(_tagIndices)); + const auto tagIndex = i->second; + + _items[tagIndex].applyOffset( + tag.start + tag.tag.size() + 1 - _currentLength); + + fillItem( + tagIndex, + text, + Edge::Close); + if (finishByNewline(tryFinishTag, text, tagIndex)) { + continue; + } + const auto position = matchPosition(tagIndex, Edge::Close); + if (position < kInvalidPosition) { + const auto till = position + tag.tag.size(); + finishTag( + tryFinishTag, + _currentLength + till, + true); + _items[tagIndex].applyOffset(till); } } for (auto i = 0, count = int(_items.size()); i != count; ++i) { - fillItem(i, text); + fillItem(i, text, Edge::Open); } - const auto min = minIndex(); + const auto min = minIndex(Edge::Open); if (min < 0) { return; } startTag( - _currentLength + matchPosition(min), + _currentLength + matchPosition(min, Edge::Open), _expressions[min].tag); } } @@ -250,13 +342,14 @@ public: } private: - void finishTag(int index, int end) { + void finishTag(int index, int end, bool closed) { Expects(_tags != nullptr); Expects(index >= 0 && index < _tags->size()); auto &tag = (*_tags)[index]; if (tag.length < 0) { tag.length = end - tag.start; + tag.closed = closed; } if (index == _currentTag) { ++_currentTag; @@ -265,7 +358,7 @@ private: bool finishByNewline( int index, const QString &text, - const std::vector &finishingIndices) { + int tagIndex) { Expects(_tags != nullptr); Expects(index >= 0 && index < _tags->size()); @@ -277,93 +370,36 @@ private: const auto endPosition = newlinePosition( text, std::max(0, tag.start + 1 - _currentLength)); - for (const auto finishingIndex : finishingIndices) { - if (matchPosition(finishingIndex) <= endPosition) { - return false; - } + if (matchPosition(tagIndex, Edge::Close) <= endPosition) { + return false; } - finishTag(index, _currentLength + endPosition); + finishTag(index, _currentLength + endPosition, false); return true; } - void bumpOffsetByTag(const InputField::PossibleTag &tag, int end) { - const auto offset = end - _currentLength; - if (tag.tag == kTagPre || tag.tag == kTagCode) { - for (auto &item : _items) { - applyOffset(item, offset); - } - } else if (tag.tag == kTagBold) { - applyOffset(_items[kTagBoldIndex], offset); - } else if (tag.tag == kTagItalic) { - applyOffset(_items[kTagItalicIndex], offset); - } else { - Unexpected("Unsupported tag."); - } - } - void applyOffset(TagStartItem &item, int offset) { - if (matchPosition(item) < offset) { - item.position = -1; - } - accumulate_max(item.offset, offset); - } void finishTags() { while (_currentTag != _currentFreeTag) { - finishTag(_currentTag, _currentLength); + finishTag(_currentTag, _currentLength, false); } } void startTag(int offset, const QString &tag) { Expects(_tags != nullptr); if (_currentFreeTag < _tags->size()) { - (*_tags)[_currentFreeTag] = { offset, -1, tag }; + (*_tags)[_currentFreeTag] = { offset, -1, false, tag }; } else { - _tags->push_back({ offset, -1, tag }); + _tags->push_back({ offset, -1, false, tag }); } ++_currentFreeTag; } - void fillItem(int index, const QString &text) { + void fillItem(int index, const QString &text, Edge edge) { Expects(index >= 0 && index < _items.size()); - auto &item = _items[index]; - if (item.position >= 0) { - return; - } - const auto length = text.size(); - const auto &expression = _expressions[index]; - const auto &tag = expression.tag; - const auto &goodBefore = expression.goodBefore; - const auto &badAfter = expression.badAfter; - const auto tagLength = tag.size(); - while (true) { - item.position = text.indexOf(tag, item.offset); - if (item.position < 0) { - item.offset = item.position = kInvalidPosition; - break; - } - item.offset = item.position + tagLength; - if (item.position > 0) { - const auto before = text[item.position - 1]; - if (expression.goodBefore.indexOf(before) < 0) { - continue; - } - } - if (item.position + tagLength < length) { - const auto after = text[item.position + tagLength]; - if (expression.badAfter.indexOf(after) >= 0) { - continue; - } - } - break; - } - item.offset = item.position + tagLength; + _items[index].fill(text, edge, _expressions[index]); } - int matchPosition(int index) const { + int matchPosition(int index, Edge edge) const { Expects(index >= 0 && index < _items.size()); - return matchPosition(_items[index]); - } - int matchPosition(const TagStartItem &item) const { - const auto position = item.position; - return (item.position >= 0) ? item.position : kInvalidPosition; + return _items[index].matchPosition(edge); } int newlinePosition(const QString &text, int offset) const { const auto length = text.size(); @@ -377,11 +413,11 @@ private: } return kInvalidPosition; } - int minIndex() const { + int minIndex(Edge edge) const { auto result = -1; auto minPosition = kInvalidPosition; for (auto i = 0, count = int(_items.size()); i != count; ++i) { - const auto position = matchPosition(i); + const auto position = matchPosition(i, edge); if (position < minPosition) { minPosition = position; result = i; @@ -389,11 +425,13 @@ private: } return result; } - int minIndex(const std::vector &indices) const { + int minIndexForFinish(const std::vector &indices) const { + const auto tagIndex = indices[0]; auto result = -1; auto minPosition = kInvalidPosition; for (auto i : indices) { - const auto position = matchPosition(i); + const auto edge = (i == tagIndex) ? Edge::Close : Edge::Open; + const auto position = matchPosition(i, edge); if (position < minPosition) { minPosition = position; result = i; @@ -402,10 +440,10 @@ private: return result; } - std::vector *_tags = nullptr; + std::vector *_tags = nullptr; const std::vector &_expressions; - const std::map> &_finishIndices; - std::vector _items; + const std::map &_tagIndices; + std::vector _items; int _currentTag = 0; int _currentFreeTag = 0; @@ -1202,7 +1240,14 @@ void InputField::setMarkdownReplacesEnabled(rpl::producer enabled) { std::move( enabled ) | rpl::start_with_next([=](bool value) { - _markdownEnabled = value; + if (_markdownEnabled != value) { + _markdownEnabled = value; + if (_markdownEnabled) { + handleContentsChanged(); + } else { + _lastMarkdownTags = {}; + } + } }, lifetime()); } @@ -1584,8 +1629,8 @@ QString InputField::getTextPart( int end, TagList &outTagsList, bool &outTagsChanged, - std::vector *outPossibleTags) const { - Expects((start == 0 && end < 0) || outPossibleTags == nullptr); + std::vector *outMarkdownTags) const { + Expects((start == 0 && end < 0) || outMarkdownTags == nullptr); if (end >= 0 && end <= start) { outTagsChanged = !outTagsList.isEmpty(); @@ -1600,8 +1645,8 @@ QString InputField::getTextPart( auto lastTag = QString(); TagAccumulator tagAccumulator(outTagsList); - PossibleTagAccumulator possibleTagAccumulator(outPossibleTags); - const auto newline = outPossibleTags ? QString(1, '\n') : QString(); + MarkdownTagAccumulator markdownTagAccumulator(outMarkdownTags); + const auto newline = outMarkdownTags ? QString(1, '\n') : QString(); const auto document = _inner->document(); const auto from = full ? document->begin() : document->findBlock(start); @@ -1669,7 +1714,7 @@ QString InputField::getTextPart( if (full || !text.isEmpty()) { lastTag = format.property(kTagProperty).toString(); tagAccumulator.feed(lastTag, result.size()); - possibleTagAccumulator.feed(text, lastTag); + markdownTagAccumulator.feed(text, lastTag); } auto begin = text.data(); @@ -1700,13 +1745,13 @@ QString InputField::getTextPart( block = block.next(); if (block != till) { result.append('\n'); - possibleTagAccumulator.feed(newline, lastTag); + markdownTagAccumulator.feed(newline, lastTag); } } tagAccumulator.feed(QString(), result.size()); tagAccumulator.finish(); - possibleTagAccumulator.finish(); + markdownTagAccumulator.finish(); outTagsChanged = tagAccumulator.changed(); return result; @@ -2031,7 +2076,9 @@ void InputField::handleContentsChanged() { -1, _lastTextWithTags.tags, tagsChanged, - _markdownEnabled ? &_textAreaPossibleTags : nullptr); + _markdownEnabled ? &_lastMarkdownTags : nullptr); + + //highlightMarkdown(); if (tagsChanged || (_lastTextWithTags.text != currentText)) { _lastTextWithTags.text = currentText; @@ -2042,6 +2089,36 @@ void InputField::handleContentsChanged() { if (App::wnd()) App::wnd()->updateGlobalMenu(); } +void InputField::highlightMarkdown() { + // Highlighting may interfere with markdown parsing -> inaccurate. + // For debug. + auto from = 0; + auto applyColor = [&](int a, int b, QColor color) { + auto cursor = textCursor(); + cursor.setPosition(a); + cursor.setPosition(b, QTextCursor::KeepAnchor); + auto format = QTextCharFormat(); + format.setForeground(color); + cursor.mergeCharFormat(format); + from = b; + }; + for (const auto &tag : _lastMarkdownTags) { + if (tag.start > from) { + applyColor(from, tag.start, QColor(0, 0, 0)); + } else if (tag.start < from) { + continue; + } + applyColor(tag.start, tag.start + tag.length, tag.closed + ? QColor(0, 128, 0) + : QColor(128, 0, 0)); + } + auto cursor = textCursor(); + cursor.movePosition(QTextCursor::End); + if (const auto till = cursor.position(); till > from) { + applyColor(from, till, QColor(0, 0, 0)); + } +} + void InputField::onUndoAvailable(bool avail) { _undoAvailable = avail; if (App::wnd()) App::wnd()->updateGlobalMenu(); @@ -2191,6 +2268,74 @@ TextWithTags InputField::getTextWithTagsPart(int start, int end) const { return result; } +TextWithTags InputField::getTextWithAppliedMarkdown() const { + if (!_markdownEnabled || _lastMarkdownTags.empty()) { + return getTextWithTags(); + } + const auto &originalText = _lastTextWithTags.text; + const auto &originalTags = _lastTextWithTags.tags; + + // Ignore tags that partially intersect some http-links. + // This will allow sending http://test.com/__test__/test correctly. + const auto links = TextUtilities::ParseEntities( + originalText, + 0).entities; + + auto result = TextWithTags(); + result.text.reserve(originalText.size()); + result.tags.reserve(originalTags.size() + _lastMarkdownTags.size()); + auto from = 0; + auto removed = 0; + auto originalTag = originalTags.begin(); + const auto originalTagsEnd = originalTags.end(); + auto link = links.begin(); + const auto linksEnd = links.end(); + for (const auto &tag : _lastMarkdownTags) { + const auto tagLength = int(tag.tag.size()); + if (!tag.closed || tag.start < from) { + continue; + } + const auto entityLength = tag.length - 2 * tagLength; + if (entityLength <= 0) { + continue; + } + while (originalTag != originalTagsEnd + && originalTag->offset + originalTag->length <= tag.start) { + result.tags.push_back(*originalTag++); + result.tags.back().offset -= removed; + } + if (originalTag != originalTagsEnd + && originalTag->offset < tag.start + tag.length) { + continue; + } + while (link != linksEnd + && link->offset() + link->length() <= tag.start) { + ++link; + } + if (link != linksEnd + && link->offset() < tag.start + tag.length + && (link->offset() + link->length() > tag.start + tag.length + || link->offset() < tag.start)) { + continue; + } + if (tag.start > from) { + result.text.append(originalText.midRef(from, tag.start - from)); + } + result.tags.push_back(TextWithTags::Tag{ + int(result.text.size()), + entityLength, + tag.tag }); + result.text.append( + originalText.midRef(tag.start + tagLength, entityLength)); + from = tag.start + tag.length; + removed += 2 * tagLength; + } + if (originalText.size() > from) { + result.text.append(originalText.midRef(from)); + } + return result; +} + void InputField::clear() { _inner->clear(); startPlaceholderAnimation(); @@ -2499,43 +2644,44 @@ const InstantReplaces &InputField::instantReplaces() const { return _mutableInstantReplaces; } +// Disable markdown instant replacement. bool InputField::processMarkdownReplaces(const QString &appended) { - if (appended.size() != 1 || !_markdownEnabled) { - return false; - } - const auto ch = appended[0]; - if (ch == '`') { - return processMarkdownReplace(kTagCode) - || processMarkdownReplace(kTagPre); - } else if (ch == '*') { - return processMarkdownReplace(kTagBold); - } else if (ch == '_') { - return processMarkdownReplace(kTagItalic); - } + //if (appended.size() != 1 || !_markdownEnabled) { + // return false; + //} + //const auto ch = appended[0]; + //if (ch == '`') { + // return processMarkdownReplace(kTagCode) + // || processMarkdownReplace(kTagPre); + //} else if (ch == '*') { + // return processMarkdownReplace(kTagBold); + //} else if (ch == '_') { + // return processMarkdownReplace(kTagItalic); + //} return false; } -bool InputField::processMarkdownReplace(const QString &tag) { - const auto position = textCursor().position(); - const auto tagLength = tag.size(); - const auto start = [&] { - for (const auto &possible : _textAreaPossibleTags) { - const auto end = possible.start + possible.length; - if (possible.start + 2 * tagLength >= position) { - return PossibleTag(); - } else if (end >= position || end + tagLength == position) { - if (possible.tag == tag) { - return possible; - } - } - } - return PossibleTag(); - }(); - if (start.tag.isEmpty()) { - return false; - } - return commitMarkdownReplacement(start.start, position, tag, tag); -} +//bool InputField::processMarkdownReplace(const QString &tag) { +// const auto position = textCursor().position(); +// const auto tagLength = tag.size(); +// const auto start = [&] { +// for (const auto &possible : _lastMarkdownTags) { +// const auto end = possible.start + possible.length; +// if (possible.start + 2 * tagLength >= position) { +// return MarkdownTag(); +// } else if (end >= position || end + tagLength == position) { +// if (possible.tag == tag) { +// return possible; +// } +// } +// } +// return MarkdownTag(); +// }(); +// if (start.tag.isEmpty()) { +// return false; +// } +// return commitMarkdownReplacement(start.start, position, tag, tag); +//} void InputField::processInstantReplaces(const QString &appended) { const auto &replaces = instantReplaces(); @@ -2549,7 +2695,7 @@ void InputField::processInstantReplaces(const QString &appended) { return; } const auto position = textCursor().position(); - for (const auto &tag : _textAreaPossibleTags) { + for (const auto &tag : _lastMarkdownTags) { if (tag.start < position && tag.start + tag.length >= position && (tag.tag == kTagCode || tag.tag == kTagPre)) { diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.h b/Telegram/SourceFiles/ui/widgets/input_fields.h index 10db179f7d..5863fbd19f 100644 --- a/Telegram/SourceFiles/ui/widgets/input_fields.h +++ b/Telegram/SourceFiles/ui/widgets/input_fields.h @@ -124,9 +124,10 @@ public: }; using TagList = TextWithTags::Tags; - struct PossibleTag { + struct MarkdownTag { int start = 0; int length = 0; + bool closed = false; QString tag; }; static const QString kTagBold; @@ -161,7 +162,11 @@ public: const TextWithTags &getTextWithTags() const { return _lastTextWithTags; } + const std::vector &getMarkdownTags() const { + return _lastMarkdownTags; + } TextWithTags getTextWithTagsPart(int start, int end = -1) const; + TextWithTags getTextWithAppliedMarkdown() const; void insertTag(const QString &text, QString tagId = QString()); bool empty() const { return _lastTextWithTags.text.isEmpty(); @@ -352,7 +357,7 @@ private: int end, TagList &outTagsList, bool &outTagsChanged, - std::vector *outPossibleTags = nullptr) const; + std::vector *outMarkdownTags = 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. @@ -366,7 +371,7 @@ private: void chopByMaxLength(int insertPosition, int insertLength); bool processMarkdownReplaces(const QString &appended); - bool processMarkdownReplace(const QString &tag); + //bool processMarkdownReplace(const QString &tag); void addMarkdownActions(not_null menu, QContextMenuEvent *e); void addMarkdownMenuAction( not_null menu, @@ -390,6 +395,8 @@ private: bool revertFormatReplace(); + void highlightMarkdown(); + const style::InputField &_st; Mode _mode = Mode::SingleLine; @@ -402,7 +409,7 @@ private: object_ptr _inner; TextWithTags _lastTextWithTags; - std::vector _textAreaPossibleTags; + std::vector _lastMarkdownTags; QString _lastPreEditText; base::lambdanotificationReplied( peerId, msgId, - _replyArea->getTextWithTags()); + _replyArea->getTextWithAppliedMarkdown()); manager()->startAllHiding(); }