Convert overlapping tags to entities and back.

This commit is contained in:
John Preston 2021-06-22 18:29:12 +04:00
parent d3eff6f38a
commit 2d71162f4a
3 changed files with 230 additions and 95 deletions

View file

@ -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<QStringRef> &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<int>(); // 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<std::pair<int, QString>>();
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;
}

View file

@ -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<QStringRef> &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);

View file

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