diff --git a/ui/text/text_entity.cpp b/ui/text/text_entity.cpp index 6dc5b37..1824cb9 100644 --- a/ui/text/text_entity.cpp +++ b/ui/text/text_entity.cpp @@ -2026,59 +2026,233 @@ bool IsMentionLink(const QStringRef &link) { return link.startsWith(kMentionTagStart); } -EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) { - EntitiesInText result; - if (tags.isEmpty()) { - return result; - } +[[nodiscard]] bool IsSeparateTag(const QStringRef &tag) { + return (tag == Ui::InputField::kTagCode) + || (tag == Ui::InputField::kTagPre); +} - result.reserve(tags.size()); - for (const auto &tag : tags) { - const auto push = [&]( - EntityType type, - const QString &data = QString()) { - result.push_back( - EntityInText(type, tag.offset, tag.length, data)); - }; - if (IsMentionLink(tag.id)) { - if (auto match = qthelp::regex_match("^(\\d+\\.\\d+)(/|$)", tag.id.midRef(kMentionTagStart.size()))) { - push(EntityType::MentionName, match->captured(1)); - } - } else if (tag.id == Ui::InputField::kTagBold) { - push(EntityType::Bold); - //} else if (tag.id == Ui::InputField::kTagSemibold) { - // push(EntityType::Semibold); // Semibold is for UI parts only. - } else if (tag.id == Ui::InputField::kTagItalic) { - push(EntityType::Italic); - } else if (tag.id == Ui::InputField::kTagUnderline) { - push(EntityType::Underline); - } else if (tag.id == Ui::InputField::kTagStrikeOut) { - push(EntityType::StrikeOut); - } else if (tag.id == Ui::InputField::kTagCode) { - push(EntityType::Code); - } else if (tag.id == Ui::InputField::kTagPre) { // #TODO entities - push(EntityType::Pre); - } else /*if (ValidateUrl(tag.id)) */{ // We validate when we insert. - push(EntityType::CustomUrl, tag.id); +QString JoinTag(const QVector &list) { + if (list.isEmpty()) { + return QString(); + } + auto length = (list.size() - 1); + for (const auto &entry : list) { + length += entry.size(); + } + auto result = QString(); + result.reserve(length); + result.append(list.front()); + for (auto i = 1, count = list.size(); i != count; ++i) { + if (!IsSeparateTag(list[i])) { + result.append('|').append(list[i]); } } return result; } -TextWithTags::Tags ConvertEntitiesToTextTags(const EntitiesInText &entities) { - TextWithTags::Tags result; +QString TagWithRemoved(const QString &tag, const QString &removed) { + if (tag == removed) { + return QString(); + } + auto list = tag.splitRef('|'); + list.erase(ranges::remove(list, removed.midRef(0)), list.end()); + return JoinTag(list); +} + +QString TagWithAdded(const QString &tag, const QString &added) { + if (tag == added) { + return tag; + } + auto list = tag.splitRef('|'); + const auto ref = added.midRef(0); + if (list.contains(ref)) { + return tag; + } + list.push_back(ref); + ranges::sort(list); + return JoinTag(list); +} + +EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) { + auto result = EntitiesInText(); + if (tags.isEmpty()) { + return result; + } + + constexpr auto kInMaskTypes = std::array{ + EntityType::Bold, + EntityType::Italic, + EntityType::Underline, + EntityType::StrikeOut, + EntityType::Code, + EntityType::Pre, + }; + struct State { + QString link; + uint32 mask = 0; + + void set(EntityType type) { + mask |= (1 << int(type)); + } + void remove(EntityType type) { + mask &= ~(1 << int(type)); + } + [[nodiscard]] bool has(EntityType type) const { + return (mask & (1 << int(type))); + } + }; + + auto offset = 0; + auto state = State(); + auto notClosedEntities = QVector(); // Stack of indices. + const auto closeOne = [&] { + Expects(!notClosedEntities.isEmpty()); + + auto &entity = result[notClosedEntities.back()]; + entity = { + entity.type(), + entity.offset(), + offset - entity.offset(), + entity.data(), + }; + if (ranges::contains(kInMaskTypes, entity.type())) { + state.remove(entity.type()); + } else { + state.link = QString(); + } + notClosedEntities.pop_back(); + }; + const auto closeType = [&](EntityType type) { + auto closeCount = 0; + const auto notClosedCount = notClosedEntities.size(); + while (closeCount < notClosedCount) { + const auto index = notClosedCount - closeCount - 1; + if (result[notClosedEntities[index]].type() == type) { + for (auto i = 0; i != closeCount + 1; ++i) { + closeOne(); + } + break; + } + ++closeCount; + } + }; + const auto openType = [&](EntityType type, const QString &data = {}) { + notClosedEntities.push_back(result.size()); + result.push_back({ type, offset, -1, data }); + }; + + const auto processState = [&](State nextState) { + const auto linkChanged = (nextState.link != state.link); + if (linkChanged) { + if (IsMentionLink(state.link)) { + closeType(EntityType::MentionName); + } else { + closeType(EntityType::CustomUrl); + } + } + for (const auto type : kInMaskTypes) { + if (state.has(type) && !nextState.has(type)) { + closeType(type); + } + } + if (linkChanged && !nextState.link.isEmpty()) { + if (IsMentionLink(nextState.link)) { + const auto match = qthelp::regex_match( + "^(\\d+\\.\\d+)(/|$)", + nextState.link.midRef(kMentionTagStart.size())); + if (match) { + openType(EntityType::MentionName, match->captured(1)); + } + } else { + openType(EntityType::CustomUrl, nextState.link); + } + } + for (const auto type : kInMaskTypes) { + if (nextState.has(type) && !state.has(type)) { + openType(type); + } + } + state = nextState; + }; + const auto stateForTag = [&](const QString &tag) { + auto result = State(); + const auto list = tag.splitRef('|'); + for (const auto &single : list) { + if (single == Ui::InputField::kTagBold) { + result.set(EntityType::Bold); + } else if (single == Ui::InputField::kTagItalic) { + result.set(EntityType::Italic); + } else if (single == Ui::InputField::kTagUnderline) { + result.set(EntityType::Underline); + } else if (single == Ui::InputField::kTagStrikeOut) { + result.set(EntityType::StrikeOut); + } else if (single == Ui::InputField::kTagCode) { + result.set(EntityType::Code); + } else if (single == Ui::InputField::kTagPre) { + result.set(EntityType::Pre); + } else { + result.link = single.toString(); + } + } + return result; + }; + auto till = offset; + for (const auto &tag : tags) { + if (tag.offset > offset) { + processState(State()); + } + offset = tag.offset; + processState(stateForTag(tag.id)); + offset += tag.length; + } + processState(State()); + + result.erase(ranges::remove_if(result, [](const EntityInText &entity) { + return (entity.length() <= 0); + }), result.end()); + + return result; +} + +TextWithTags::Tags ConvertEntitiesToTextTags( + const EntitiesInText &entities) { + auto result = TextWithTags::Tags(); if (entities.isEmpty()) { return result; } - result.reserve(entities.size()); + auto offset = 0; + auto current = QString(); + const auto updateCurrent = [&](int nextOffset, const QString &next) { + if (next == current) { + return; + } else if (nextOffset > offset) { + result.push_back({ offset, nextOffset - offset, current }); + offset = nextOffset; + } + current = next; + }; + auto toRemove = std::vector>(); + const auto removeTill = [&](int nextOffset) { + while (!toRemove.empty() && toRemove.front().first <= nextOffset) { + updateCurrent( + toRemove.front().first, + TagWithRemoved(current, toRemove.front().second)); + toRemove.erase(toRemove.begin()); + } + }; for (const auto &entity : entities) { const auto push = [&](const QString &tag) { - result.push_back({ entity.offset(), entity.length(), tag }); + removeTill(entity.offset()); + updateCurrent(entity.offset(), TagWithAdded(current, tag)); + toRemove.push_back({ offset + entity.length(), tag }); + ranges::sort(toRemove); }; switch (entity.type()) { case EntityType::MentionName: { - auto match = QRegularExpression(R"(^(\d+\.\d+)$)").match(entity.data()); + auto match = QRegularExpression( + R"(^(\d+\.\d+)$)" + ).match(entity.data()); if (match.hasMatch()) { push(kMentionTagStart + entity.data()); } @@ -2105,6 +2279,9 @@ TextWithTags::Tags ConvertEntitiesToTextTags(const EntitiesInText &entities) { case EntityType::Pre: push(Ui::InputField::kTagPre); break; } } + if (!toRemove.empty()) { + removeTill(toRemove.back().first); + } return result; } diff --git a/ui/text/text_entity.h b/ui/text/text_entity.h index 0fb3388..20be4ce 100644 --- a/ui/text/text_entity.h +++ b/ui/text/text_entity.h @@ -363,6 +363,18 @@ inline const auto kMentionTagStart = qstr("mention://user."); [[nodiscard]] inline bool IsMentionLink(const QString &link) { return IsMentionLink(link.midRef(0)); } + +[[nodiscard]] bool IsSeparateTag(const QStringRef &tag); +[[nodiscard]] inline bool IsSeparateTag(const QString &tag) { + return IsSeparateTag(tag.midRef(0)); +} + +[[nodiscard]] QString JoinTag(const QVector &list); +[[nodiscard]] QString TagWithRemoved( + const QString &tag, + const QString &removed); +[[nodiscard]] QString TagWithAdded(const QString &tag, const QString &added); + EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags); TextWithTags::Tags ConvertEntitiesToTextTags( const EntitiesInText &entities); diff --git a/ui/widgets/input_fields.cpp b/ui/widgets/input_fields.cpp index df2e22a..40e9a1c 100644 --- a/ui/widgets/input_fields.cpp +++ b/ui/widgets/input_fields.cpp @@ -98,60 +98,6 @@ bool IsNewline(QChar ch) { return (kNewlineChars.indexOf(ch) >= 0); } -[[nodiscard]] bool IsSeparateTag(const QStringRef &tag) { - return (tag == kTagCode.midRef(0)) || (tag == kTagPre.midRef(0)); -} - -[[nodiscard]] bool IsSeparateTag(const QString &tag) { - return IsSeparateTag(tag.midRef(0)); -} - -[[nodiscard]] QString JoinTag(const QVector &list) { - if (list.isEmpty()) { - return QString(); - } - auto length = (list.size() - 1); - for (const auto &entry : list) { - length += entry.size(); - } - auto result = QString(); - result.reserve(length); - result.append(list.front()); - for (auto i = 1, count = list.size(); i != count; ++i) { - if (!IsSeparateTag(list[i])) { - result.append('|').append(list[i]); - } - } - return result; -} - -[[nodiscard]] QString TagWithRemoved( - const QString &tag, - const QString &removed) { - if (tag == removed) { - return QString(); - } - auto list = tag.splitRef('|'); - list.erase(ranges::remove(list, removed.midRef(0)), list.end()); - return JoinTag(list); -} - -[[nodiscard]] QString TagWithAdded( - const QString &tag, - const QString &added) { - if (tag == added) { - return tag; - } - auto list = tag.splitRef('|'); - const auto ref = added.midRef(0); - if (list.contains(ref)) { - return tag; - } - list.push_back(ref); - ranges::sort(list); - return JoinTag(list); -} - [[nodiscard]] bool IsValidMarkdownLink(const QStringRef &link) { return (link.indexOf('.') >= 0) || (link.indexOf(':') >= 0); } @@ -3342,7 +3288,7 @@ void InputField::addMarkdownTag( auto tags = TagList(); auto filled = 0; const auto add = [&](const TextWithTags::Tag &existing) { - const auto id = TagWithAdded(existing.id, tag); + const auto id = TextUtilities::TagWithAdded(existing.id, tag); tags.push_back({ existing.offset, existing.length, id }); filled = std::clamp( existing.offset + existing.length, @@ -3357,7 +3303,7 @@ void InputField::addMarkdownTag( id, }); }; - if (!IsSeparateTag(tag)) { + if (!TextUtilities::IsSeparateTag(tag)) { for (const auto &existing : current.tags) { if (existing.offset >= till) { break; @@ -3388,7 +3334,7 @@ void InputField::removeMarkdownTag( auto tags = TagList(); for (const auto &existing : current.tags) { - const auto id = TagWithRemoved(existing.id, tag); + const auto id = TextUtilities::TagWithRemoved(existing.id, tag); if (!id.isEmpty()) { tags.push_back({ existing.offset, existing.length, id }); }