From eaf91bba5830f8a83304cf0a20c978e617ed2fc0 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 6 Jul 2017 16:44:11 +0300 Subject: [PATCH] Parse bold / italic markdown entities. --- .../history/history_admin_log_inner.cpp | 2 +- .../history/history_media_types.cpp | 2 +- Telegram/SourceFiles/layout.cpp | 4 +- .../SourceFiles/overview/overview_layout.cpp | 2 +- Telegram/SourceFiles/ui/text/text.cpp | 6 +- Telegram/SourceFiles/ui/text/text_entity.cpp | 422 +++++++++++------- Telegram/SourceFiles/ui/text/text_entity.h | 16 +- Telegram/SourceFiles/ui/widgets/labels.cpp | 2 +- 8 files changed, 272 insertions(+), 184 deletions(-) diff --git a/Telegram/SourceFiles/history/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/history_admin_log_inner.cpp index 26ec82a66..083d635dc 100644 --- a/Telegram/SourceFiles/history/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/history_admin_log_inner.cpp @@ -386,7 +386,7 @@ void InnerWidget::clearAndRequestLog() { void InnerWidget::updateEmptyText() { auto options = _defaultOptions; - options.flags |= TextParseMono; // For bold :/ + options.flags |= TextParseMarkdown; auto hasSearch = !_searchQuery.isEmpty(); auto hasFilter = (_filter.flags != 0) || !_filter.allUsers; auto text = TextWithEntities { lang((hasSearch || hasFilter) ? lng_admin_log_no_results_title : lng_admin_log_no_events_title) }; diff --git a/Telegram/SourceFiles/history/history_media_types.cpp b/Telegram/SourceFiles/history/history_media_types.cpp index 4e7ae013e..91bbe1ccc 100644 --- a/Telegram/SourceFiles/history/history_media_types.cpp +++ b/Telegram/SourceFiles/history/history_media_types.cpp @@ -49,7 +49,7 @@ TextParseOptions _webpageTitleOptions = { Qt::LayoutDirectionAuto, // dir }; TextParseOptions _webpageDescriptionOptions = { - TextParseLinks | TextParseMentions | TextParseHashtags | TextParseMultiline | TextParseRichText | TextParseMono, // flags + TextParseLinks | TextParseMentions | TextParseHashtags | TextParseMultiline | TextParseRichText | TextParseMarkdown, // flags 0, // maxw 0, // maxh Qt::LayoutDirectionAuto, // dir diff --git a/Telegram/SourceFiles/layout.cpp b/Telegram/SourceFiles/layout.cpp index adef15a06..89e67b03c 100644 --- a/Telegram/SourceFiles/layout.cpp +++ b/Telegram/SourceFiles/layout.cpp @@ -44,13 +44,13 @@ TextParseOptions _textDlgOptions = { Qt::LayoutDirectionAuto, // lang-dependent }; TextParseOptions _historyTextOptions = { - TextParseLinks | TextParseMentions | TextParseHashtags | TextParseMultiline | TextParseRichText | TextParseMono, // flags + TextParseLinks | TextParseMentions | TextParseHashtags | TextParseMultiline | TextParseRichText | TextParseMarkdown, // flags 0, // maxw 0, // maxh Qt::LayoutDirectionAuto, // dir }; TextParseOptions _historyBotOptions = { - TextParseLinks | TextParseMentions | TextParseHashtags | TextParseBotCommands | TextParseMultiline | TextParseRichText | TextParseMono, // flags + TextParseLinks | TextParseMentions | TextParseHashtags | TextParseBotCommands | TextParseMultiline | TextParseRichText | TextParseMarkdown, // flags 0, // maxw 0, // maxh Qt::LayoutDirectionAuto, // dir diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp index 8ca69432e..e63f47b28 100644 --- a/Telegram/SourceFiles/overview/overview_layout.cpp +++ b/Telegram/SourceFiles/overview/overview_layout.cpp @@ -41,7 +41,7 @@ namespace Layout { namespace { TextParseOptions _documentNameOptions = { - TextParseMultiline | TextParseRichText | TextParseLinks | TextParseHashtags | TextParseMentions | TextParseBotCommands | TextParseMono, // flags + TextParseMultiline | TextParseRichText | TextParseLinks | TextParseHashtags | TextParseMentions | TextParseBotCommands | TextParseMarkdown, // flags 0, // maxw 0, // maxh Qt::LayoutDirectionAuto, // dir diff --git a/Telegram/SourceFiles/ui/text/text.cpp b/Telegram/SourceFiles/ui/text/text.cpp index 99eda4a7a..5e3bed9fb 100644 --- a/Telegram/SourceFiles/ui/text/text.cpp +++ b/Telegram/SourceFiles/ui/text/text.cpp @@ -513,8 +513,8 @@ public: bool parseMentions = (options.flags & TextParseMentions); bool parseHashtags = (options.flags & TextParseHashtags); bool parseBotCommands = (options.flags & TextParseBotCommands); - bool parseMono = (options.flags & TextParseMono); - if (!parseMentions || !parseHashtags || !parseBotCommands || !parseMono) { + bool parseMarkdown = (options.flags & TextParseMarkdown); + if (!parseMentions || !parseHashtags || !parseBotCommands || !parseMarkdown) { int32 i = 0, l = preparsed.size(); source.entities.clear(); source.entities.reserve(l); @@ -524,7 +524,7 @@ public: if (((type == EntityInTextMention || type == EntityInTextMentionName) && !parseMentions) || (type == EntityInTextHashtag && !parseHashtags) || (type == EntityInTextBotCommand && !parseBotCommands) || - ((type == EntityInTextBold || type == EntityInTextItalic || type == EntityInTextCode || type == EntityInTextPre) && !parseMono)) { + ((type == EntityInTextBold || type == EntityInTextItalic || type == EntityInTextCode || type == EntityInTextPre) && !parseMarkdown)) { continue; } source.entities.push_back(preparsed.at(i)); diff --git a/Telegram/SourceFiles/ui/text/text_entity.cpp b/Telegram/SourceFiles/ui/text/text_entity.cpp index be56202bd..3bb9ecd34 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.cpp +++ b/Telegram/SourceFiles/ui/text/text_entity.cpp @@ -43,24 +43,40 @@ QString ExpressionMailNameAtEnd() { return qsl("[a-zA-Z\\-_\\.0-9]{1,256}$"); } +QString ExpressionSeparators(const QString &additional) { + return qsl("\\s\\.,:;<>|'\"\\[\\]\\{\\}\\~\\!\\%\\^\\(\\)\\-\\+=\\x10") + additional; +} + QString ExpressionHashtag() { - return qsl("(^|[\\s\\.,:;<>|'\"\\[\\]\\{\\}`\\~\\!\\%\\^\\*\\(\\)\\-\\+=\\x10])#[\\w]{2,64}([\\W]|$)"); + return qsl("(^|[") + ExpressionSeparators(qsl("`\\*/")) + qsl("])#[\\w]{2,64}([\\W]|$)"); } QString ExpressionMention() { - return qsl("(^|[\\s\\.,:;<>|'\"\\[\\]\\{\\}`\\~\\!\\%\\^\\*\\(\\)\\-\\+=\\x10])@[A-Za-z_0-9]{1,32}([\\W]|$)"); + return qsl("(^|[") + ExpressionSeparators(qsl("`\\*/")) + qsl("])@[A-Za-z_0-9]{1,32}([\\W]|$)"); } QString ExpressionBotCommand() { - return qsl("(^|[\\s\\.,:;<>|'\"\\[\\]\\{\\}`\\~\\!\\%\\^\\*\\(\\)\\-\\+=\\x10])/[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 ExpressionMonoInline() { // pre - return qsl("(^|[\\s\\.,:;<>|'\"\\[\\]\\{\\}`\\~\\!\\?\\%\\^\\*\\(\\)\\-\\+=\\x10])(````?)[\\s\\S]+?(````?)([\\s\\.,:;<>|'\"\\[\\]\\{\\}`\\~\\!\\?\\%\\^\\*\\(\\)\\-\\+=\\x10]|$)"); +QString ExpressionMarkdownBold() { + auto separators = ExpressionSeparators(qsl("`/")); + return qsl("(^|[") + separators + qsl("])(\\*\\*)[\\s\\S]+?(\\*\\*)([") + separators + qsl("]|$)"); } -QString ExpressionMonoBlock() { // code - return qsl("(^|[\\s\\.,:;<>|'\"\\[\\]\\{\\}`\\~\\!\\?\\%\\^\\*\\(\\)\\-\\+=\\x10])(`)[^\\n]+?(`)([\\s\\.,:;<>|'\"\\[\\]\\{\\}`\\~\\!\\?\\%\\^\\*\\(\\)\\-\\+=\\x10]|$)"); +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) { @@ -1146,13 +1162,23 @@ const QRegularExpression &RegExpBotCommand() { return result; } -const QRegularExpression &RegExpMonoInline() { - static const auto result = CreateRegExp(ExpressionMonoInline()); +const QRegularExpression &RegExpMarkdownBold() { + static const auto result = CreateRegExp(ExpressionMarkdownBold()); return result; } -const QRegularExpression &RegExpMonoBlock() { - static const auto result = CreateRegExp(ExpressionMonoBlock()); +const QRegularExpression &RegExpMarkdownItalic() { + static const auto result = CreateRegExp(ExpressionMarkdownItalic()); + return result; +} + +const QRegularExpression &RegExpMarkdownMonoInline() { + static const auto result = CreateRegExp(ExpressionMarkdownMonoInline()); + return result; +} + +const QRegularExpression &RegExpMarkdownMonoBlock() { + static const auto result = CreateRegExp(ExpressionMarkdownMonoBlock()); return result; } @@ -1471,22 +1497,25 @@ EntitiesInText EntitiesFromMTP(const QVector &entities) { return result; } -MTPVector EntitiesToMTP(const EntitiesInText &links, ConvertOption option) { +MTPVector EntitiesToMTP(const EntitiesInText &entities, ConvertOption option) { auto v = QVector(); - v.reserve(links.size()); - for_const (auto &link, links) { - if (link.length() <= 0) continue; + v.reserve(entities.size()); + for_const (auto &entity, entities) { + if (entity.length() <= 0) continue; if (option == ConvertOption::SkipLocal - && link.type() != EntityInTextCode - && link.type() != EntityInTextPre - && link.type() != EntityInTextMentionName) { + && entity.type() != EntityInTextBold + && entity.type() != EntityInTextItalic + && entity.type() != EntityInTextCode + && entity.type() != EntityInTextPre + && entity.type() != EntityInTextMentionName) { continue; } - auto offset = MTP_int(link.offset()), length = MTP_int(link.length()); - switch (link.type()) { + auto offset = MTP_int(entity.offset()); + auto length = MTP_int(entity.length()); + switch (entity.type()) { case EntityInTextUrl: v.push_back(MTP_messageEntityUrl(offset, length)); break; - case EntityInTextCustomUrl: v.push_back(MTP_messageEntityTextUrl(offset, length, MTP_string(link.data()))); break; + case EntityInTextCustomUrl: v.push_back(MTP_messageEntityTextUrl(offset, length, MTP_string(entity.data()))); break; case EntityInTextEmail: v.push_back(MTP_messageEntityEmail(offset, length)); break; case EntityInTextHashtag: v.push_back(MTP_messageEntityHashtag(offset, length)); break; case EntityInTextMention: v.push_back(MTP_messageEntityMention(offset, length)); break; @@ -1499,7 +1528,7 @@ MTPVector EntitiesToMTP(const EntitiesInText &links, ConvertOp return MTP_inputUser(MTP_int(fields.userId), MTP_long(fields.accessHash)); } return MTP_inputUserEmpty(); - })(link.data()); + })(entity.data()); if (inputUser.type() != mtpc_inputUserEmpty) { v.push_back(MTP_inputMessageEntityMentionName(offset, length, inputUser)); } @@ -1508,172 +1537,225 @@ MTPVector EntitiesToMTP(const EntitiesInText &links, ConvertOp case EntityInTextBold: v.push_back(MTP_messageEntityBold(offset, length)); break; case EntityInTextItalic: v.push_back(MTP_messageEntityItalic(offset, length)); break; case EntityInTextCode: v.push_back(MTP_messageEntityCode(offset, length)); break; - case EntityInTextPre: v.push_back(MTP_messageEntityPre(offset, length, MTP_string(link.data()))); break; + case EntityInTextPre: v.push_back(MTP_messageEntityPre(offset, length, MTP_string(entity.data()))); break; } } return MTP_vector(std::move(v)); } -// Some code is duplicated in flattextarea.cpp! -void ParseEntities(TextWithEntities &result, int32 flags, bool rich) { - auto newEntities = EntitiesInText(); +struct MarkdownPart { + EntityInTextType type = EntityInTextInvalid; + int outerStart = 0; + int innerStart = 0; + int innerEnd = 0; + int outerEnd = 0; + bool addNewlineBefore = false; + bool addNewlineAfter = false; +}; - bool withHashtags = (flags & TextParseHashtags); - bool withMentions = (flags & TextParseMentions); - bool withBotCommands = (flags & TextParseBotCommands); - bool withMono = (flags & TextParseMono); +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 (withMono) { // parse mono entities (code and pre) - int existingEntityIndex = 0, existingEntitiesCount = result.entities.size(); - int existingEntityShiftLeft = 0; + auto match = regexp().match(text, matchFromOffset); + if (!match.hasMatch()) { + return result; + } - QString newText; + 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; +} - int32 offset = 0, matchOffset = offset, len = result.text.size(), commandOffset = rich ? 0 : len; - bool inLink = false, commandIsLink = false; - const QChar *start = result.text.constData(); - for (; matchOffset < len;) { - if (commandOffset <= matchOffset) { - for (commandOffset = matchOffset; commandOffset < len; ++commandOffset) { - if (*(start + commandOffset) == TextCommand) { - inLink = commandIsLink; - commandIsLink = textcmdStartsLink(start, len, commandOffset); - break; - } - } - if (commandOffset >= len) { +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, bool rich) { + if (result.empty()) { + return; + } + auto newResult = TextWithEntities(); + + 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 = false; - } - } - auto mPre = RegExpMonoInline().match(result.text, matchOffset); - auto mCode = RegExpMonoBlock().match(result.text, matchOffset); - if (!mPre.hasMatch() && !mCode.hasMatch()) break; - - int preStart = mPre.hasMatch() ? mPre.capturedStart() : INT_MAX, - preEnd = mPre.hasMatch() ? mPre.capturedEnd() : INT_MAX, - codeStart = mCode.hasMatch() ? mCode.capturedStart() : INT_MAX, - codeEnd = mCode.hasMatch() ? mCode.capturedEnd() : INT_MAX, - tagStart, tagEnd; - if (mPre.hasMatch()) { - if (!mPre.capturedRef(1).isEmpty()) { - ++preStart; - } - if (!mPre.capturedRef(4).isEmpty()) { - --preEnd; - } - } - if (mCode.hasMatch()) { - if (!mCode.capturedRef(1).isEmpty()) { - ++codeStart; - } - if (!mCode.capturedRef(4).isEmpty()) { - --codeEnd; - } - } - - bool pre = (preStart <= codeStart); - auto mTag = pre ? mPre : mCode; - if (pre) { - tagStart = preStart; - tagEnd = preEnd; - } else { - tagStart = codeStart; - tagEnd = codeEnd; - } - - bool inCommand = checkTagStartInCommand(start, len, tagStart, commandOffset, commandIsLink, inLink); - if (inCommand || inLink) { - matchOffset = commandOffset; - continue; - } - - bool addNewlineBefore = false, addNewlineAfter = false; - int32 outerStart = tagStart, outerEnd = tagEnd; - int32 innerStart = tagStart + mTag.capturedLength(2), innerEnd = tagEnd - mTag.capturedLength(3); - - // Check if start or end sequences intersect any existing entity. - int intersectedEntityEnd = 0; - for_const (auto &entity, result.entities) { - if (qMin(innerStart, entity.offset() + entity.length()) > qMax(outerStart, entity.offset()) || - qMin(outerEnd, entity.offset() + entity.length()) > qMax(innerEnd, entity.offset())) { - intersectedEntityEnd = entity.offset() + entity.length(); + commandIsLink = textcmdStartsLink(start, length, nextCommandOffset); break; } } - if (intersectedEntityEnd > 0) { - matchOffset = qMax(innerStart, intersectedEntityEnd); - continue; + if (nextCommandOffset >= length) { + inLink = commandIsLink; + commandIsLink = false; } - - if (newText.isEmpty()) newText.reserve(result.text.size()); - if (pre) { - while (outerStart > 0 && chIsSpace(*(start + outerStart - 1), rich) && !chIsNewline(*(start + outerStart - 1))) { - --outerStart; - } - addNewlineBefore = (outerStart > 0 && !chIsNewline(*(start + outerStart - 1))); - - for (int32 testInnerStart = innerStart; testInnerStart < innerEnd; ++testInnerStart) { - if (chIsNewline(*(start + testInnerStart))) { - innerStart = testInnerStart + 1; - break; - } else if (!chIsSpace(*(start + testInnerStart))) { - break; - } - } - for (int32 testInnerEnd = innerEnd; innerStart < testInnerEnd;) { - --testInnerEnd; - if (chIsNewline(*(start + testInnerEnd))) { - innerEnd = testInnerEnd; - break; - } else if (!chIsSpace(*(start + testInnerEnd))) { - break; - } - } - - while (outerEnd < len && chIsSpace(*(start + outerEnd)) && !chIsNewline(*(start + outerEnd))) { - ++outerEnd; - } - addNewlineAfter = (outerEnd < len && !chIsNewline(*(start + outerEnd))); - } - - for (; existingEntityIndex < existingEntitiesCount && result.entities[existingEntityIndex].offset() < innerStart; ++existingEntityIndex) { - auto &entity = result.entities[existingEntityIndex]; - newEntities.push_back(entity); - newEntities.back().shiftLeft(existingEntityShiftLeft); - } - if (outerStart > offset) newText.append(start + offset, outerStart - offset); - if (addNewlineBefore) newText.append('\n'); - existingEntityShiftLeft += (innerStart - outerStart) - (addNewlineBefore ? 1 : 0); - - int entityStart = newText.size(), entityLength = innerEnd - innerStart; - newEntities.push_back(EntityInText(pre ? EntityInTextPre : EntityInTextCode, entityStart, entityLength)); - - for (; existingEntityIndex < existingEntitiesCount && result.entities[existingEntityIndex].offset() <= innerEnd; ++existingEntityIndex) { - auto &entity = result.entities[existingEntityIndex]; - newEntities.push_back(entity); - newEntities.back().shiftLeft(existingEntityShiftLeft); - } - newText.append(start + innerStart, entityLength); - if (addNewlineAfter) newText.append('\n'); - existingEntityShiftLeft += (outerEnd - innerEnd) - (addNewlineAfter ? 1 : 0); - - offset = matchOffset = outerEnd; } - if (!newText.isEmpty()) { - newText.append(start + offset, len - offset); - result.text = newText; - } - if (!newEntities.isEmpty()) { - for (; existingEntityIndex < existingEntitiesCount; ++existingEntityIndex) { - auto &entity = result.entities[existingEntityIndex]; - newEntities.push_back(entity); - newEntities.back().shiftLeft(existingEntityShiftLeft); + auto part = MarkdownPart(); + auto testPart = [&part, &result, matchFromOffset, rich](EntityInTextType type) { + auto test = GetMarkdownPart(type, result.text, matchFromOffset, rich); + if (test.type != EntityInTextInvalid) { + if (part.type == EntityInTextInvalid || part.outerStart > test.outerStart) { + part = test; + } } - result.entities = newEntities; - newEntities = EntitiesInText(); + }; + testPart(EntityInTextBold); + testPart(EntityInTextItalic); + testPart(EntityInTextPre); + testPart(EntityInTextCode); + 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; + } + } + 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); + } +} + +// 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) + ParseMarkdown(result, rich); + } + + auto newEntities = EntitiesInText(); + bool withHashtags = (flags & TextParseHashtags); + bool withMentions = (flags & TextParseMentions); + bool withBotCommands = (flags & TextParseBotCommands); int existingEntityIndex = 0, existingEntitiesCount = result.entities.size(); int existingEntityEnd = 0; diff --git a/Telegram/SourceFiles/ui/text/text_entity.h b/Telegram/SourceFiles/ui/text/text_entity.h index 3091787ee..7d53f484b 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.h +++ b/Telegram/SourceFiles/ui/text/text_entity.h @@ -115,6 +115,10 @@ private: struct TextWithEntities { QString text; EntitiesInText entities; + + bool empty() const { + return text.isEmpty(); + } }; enum { @@ -124,7 +128,7 @@ enum { TextParseMentions = 0x008, TextParseHashtags = 0x010, TextParseBotCommands = 0x020, - TextParseMono = 0x040, + TextParseMarkdown = 0x040, TextTwitterMentions = 0x100, TextTwitterHashtags = 0x200, @@ -145,8 +149,10 @@ const QRegularExpression &RegExpMailNameAtEnd(); const QRegularExpression &RegExpHashtag(); const QRegularExpression &RegExpMention(); const QRegularExpression &RegExpBotCommand(); -const QRegularExpression &RegExpMonoInline(); -const QRegularExpression &RegExpMonoBlock(); +const QRegularExpression &RegExpMarkdownBold(); +const QRegularExpression &RegExpMarkdownItalic(); +const QRegularExpression &RegExpMarkdownMonoInline(); +const QRegularExpression &RegExpMarkdownMonoBlock(); inline void Append(TextWithEntities &to, TextWithEntities &&append) { auto entitiesShiftRight = to.text.size(); @@ -192,10 +198,10 @@ enum class ConvertOption { WithLocal, SkipLocal, }; -MTPVector EntitiesToMTP(const EntitiesInText &links, ConvertOption option = ConvertOption::WithLocal); +MTPVector EntitiesToMTP(const EntitiesInText &entities, ConvertOption option = ConvertOption::WithLocal); // New entities are added to the ones that are already in result. -// Changes text if (flags & TextParseMono). +// Changes text if (flags & TextParseMarkdown). void ParseEntities(TextWithEntities &result, int32 flags, bool rich = false); QString ApplyEntities(const TextWithEntities &text); diff --git a/Telegram/SourceFiles/ui/widgets/labels.cpp b/Telegram/SourceFiles/ui/widgets/labels.cpp index d97acdb7c..ec617bb41 100644 --- a/Telegram/SourceFiles/ui/widgets/labels.cpp +++ b/Telegram/SourceFiles/ui/widgets/labels.cpp @@ -35,7 +35,7 @@ TextParseOptions _labelOptions = { }; TextParseOptions _labelMarkedOptions = { - TextParseMultiline | TextParseRichText | TextParseLinks | TextParseHashtags | TextParseMentions | TextParseBotCommands | TextParseMono, // flags + TextParseMultiline | TextParseRichText | TextParseLinks | TextParseHashtags | TextParseMentions | TextParseBotCommands | TextParseMarkdown, // flags 0, // maxw 0, // maxh Qt::LayoutDirectionAuto, // dir