Files
AyuGramDesktop/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp
T
2026-07-10 14:04:29 +04:00

9309 lines
254 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "iv/editor/iv_editor_state.h"
#include "iv/editor/iv_editor_text_entities.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include <algorithm>
#include <limits>
#include <utility>
namespace Iv::Editor {
namespace {
using Block = RichPage::Block;
using BlockContainerKind = State::BlockContainerKind;
using BlockContainerPath = State::BlockContainerPath;
using ApplyResult = State::ApplyResult;
using BlockKind = RichPage::BlockKind;
using BoundaryAction = State::BoundaryTarget::Action;
using BlockPath = State::BlockPath;
using FieldMode = State::FieldMode;
using InsertBlockType = State::InsertBlockType;
using InsertionAnchor = State::InsertionAnchor;
using LeafKind = State::LeafKind;
using LeafPath = State::LeafPath;
using ListStyle = State::ListStyle;
using ListItem = RichPage::ListItem;
using ListKind = RichPage::ListKind;
using NativeInstantViewLeafUpdateResult
= Markdown::NativeInstantViewLeafUpdateResult;
using PreparedBlockContainerKind = Markdown::PreparedEditBlockContainerKind;
using PreparedEditLeafKind = Markdown::PreparedEditLeafKind;
using PreparedEditLeafSource = Markdown::PreparedEditLeafSource;
using PreparedEditListItemRange = Markdown::PreparedEditListItemRange;
using PreparedEditListItemSource = Markdown::PreparedEditListItemSource;
using PreparedEditSelectionKind = Markdown::PreparedEditSelectionKind;
using PreparedBlockContainerPath = Markdown::PreparedEditBlockContainerPath;
using PreparedBlockPath = Markdown::PreparedEditBlockPath;
using PreparedBlockContainerStep = Markdown::PreparedEditBlockContainerStep;
using PreparedEditSelection = Markdown::PreparedEditSelection;
using PreparedMutationKind = State::PreparedMutationKind;
using PreparedOrderedListType = Markdown::PreparedOrderedListType;
using ReplaceTarget = State::ReplaceTarget;
using RemovalKind = State::RemovalKind;
using RemovalTarget = State::RemovalTarget;
using RichText = RichPage::RichText;
using OrderedListData = RichPage::OrderedListData;
using TableCell = RichPage::TableCell;
using TableRow = RichPage::TableRow;
using TaskState = RichPage::TaskState;
using TextNodeDescriptor = State::TextNodeDescriptor;
using TextFormattingAction = State::TextFormattingAction;
using TextSelectionDropResult = State::TextSelectionDropResult;
using TextNodeSpan = State::TextNodeSpan;
struct TextRange {
int offset = 0;
int length = 0;
};
constexpr auto kMaxRichTextNodeLength = 16000;
constexpr auto kMaxCommittedFieldLength = 256 * 1024;
[[nodiscard]] std::vector<TextWithEntities> SplitFieldText(
TextWithEntities text) {
auto result = std::vector<TextWithEntities>();
auto left = std::move(text);
auto consumed = 0;
while (!left.text.isEmpty() && consumed < kMaxCommittedFieldLength) {
auto part = TextWithEntities();
const auto limit = std::min(
kMaxRichTextNodeLength,
kMaxCommittedFieldLength - consumed);
if (!TextUtilities::CutPart(part, left, limit)
|| part.text.isEmpty()) {
break;
}
consumed += part.text.size();
result.push_back(std::move(part));
}
return result;
}
[[nodiscard]] TextWithEntities JoinText(
TextWithEntities before,
TextWithEntities selected,
TextWithEntities after) {
before.append(std::move(selected));
before.append(std::move(after));
return before;
}
[[nodiscard]] bool RangeInsideText(
const QString &text,
int offset,
int length) {
return (offset >= 0)
&& (length >= 0)
&& (offset <= text.size())
&& ((offset + length) <= text.size());
}
[[nodiscard]] const QString *FormattingActionTag(
TextFormattingAction action) {
switch (action) {
case TextFormattingAction::Bold:
return &Ui::InputField::kTagBold;
case TextFormattingAction::Italic:
return &Ui::InputField::kTagItalic;
case TextFormattingAction::Underline:
return &Ui::InputField::kTagUnderline;
case TextFormattingAction::StrikeOut:
return &Ui::InputField::kTagStrikeOut;
case TextFormattingAction::Spoiler:
return &Ui::InputField::kTagSpoiler;
case TextFormattingAction::PlainText:
return nullptr;
}
return nullptr;
}
[[nodiscard]] QString TagWithoutInstantViewMath(QStringView tag) {
return TextUtilities::TagWithRemoved(
tag.toString(),
Ui::InputField::kTagIvMath);
}
[[nodiscard]] QString TagWithAddedDroppingMath(
const QString &tag,
const QString &added) {
if (added == Ui::InputField::kTagIvMath) {
return Ui::InputField::kTagIvMath;
}
return TextUtilities::TagWithAdded(
TagWithoutInstantViewMath(tag),
added);
}
void SortTags(TextWithTags::Tags *tags) {
std::sort(tags->begin(), tags->end(), [](const auto &a, const auto &b) {
if (a.offset != b.offset) {
return a.offset < b.offset;
} else if (a.length != b.length) {
return a.length < b.length;
}
return a.id < b.id;
});
}
[[nodiscard]] bool TagContains(QStringView tags, QStringView tagId) {
return TextUtilities::SplitTags(tags).contains(tagId);
}
[[nodiscard]] bool HasFullTextTag(
const TextWithTags &textWithTags,
const QString &tag) {
if (tag.isEmpty() || textWithTags.text.isEmpty()) {
return false;
}
auto ranges = std::vector<TextRange>();
ranges.reserve(textWithTags.tags.size());
for (const auto &existing : textWithTags.tags) {
if (existing.length <= 0
|| !RangeInsideText(
textWithTags.text,
existing.offset,
existing.length)
|| !TagContains(existing.id, tag)) {
continue;
}
ranges.push_back({
.offset = existing.offset,
.length = existing.length,
});
}
if (ranges.empty()) {
return false;
}
std::sort(ranges.begin(), ranges.end(), [](const auto &a, const auto &b) {
if (a.offset != b.offset) {
return a.offset < b.offset;
}
return a.length < b.length;
});
auto coveredTill = 0;
for (const auto &range : ranges) {
if (range.offset > coveredTill) {
return false;
}
coveredTill = std::max(coveredTill, range.offset + range.length);
if (coveredTill >= textWithTags.text.size()) {
return true;
}
}
return (coveredTill >= textWithTags.text.size());
}
void OverlayTag(
TextWithTags::Tags *tags,
const TextWithTags::Tag &overlay,
const QString &text) {
if (overlay.id.isEmpty()
|| overlay.length <= 0
|| !RangeInsideText(text, overlay.offset, overlay.length)) {
return;
}
const auto from = overlay.offset;
const auto till = from + overlay.length;
auto coveredTill = from;
auto result = TextWithTags::Tags();
result.reserve(tags->size() + 3);
for (const auto &tag : *tags) {
const auto tagFrom = tag.offset;
const auto tagTill = tag.offset + tag.length;
if (tagTill <= from) {
result.push_back(tag);
continue;
} else if (tagFrom >= till) {
if (coveredTill < till) {
result.push_back({
.offset = coveredTill,
.length = till - coveredTill,
.id = overlay.id,
});
coveredTill = till;
}
result.push_back(tag);
continue;
}
if (tagFrom > coveredTill) {
result.push_back({
.offset = coveredTill,
.length = tagFrom - coveredTill,
.id = overlay.id,
});
coveredTill = tagFrom;
}
if (tagFrom < from) {
result.push_back({
.offset = tagFrom,
.length = from - tagFrom,
.id = tag.id,
});
}
const auto middleFrom = std::max(tagFrom, from);
const auto middleTill = std::min(tagTill, till);
if (middleFrom < middleTill) {
result.push_back({
.offset = middleFrom,
.length = middleTill - middleFrom,
.id = TagWithAddedDroppingMath(tag.id, overlay.id),
});
coveredTill = middleTill;
}
if (tagTill > till) {
result.push_back({
.offset = till,
.length = tagTill - till,
.id = tag.id,
});
}
}
if (coveredTill < till) {
result.push_back({
.offset = coveredTill,
.length = till - coveredTill,
.id = overlay.id,
});
}
SortTags(&result);
*tags = TextUtilities::SimplifyTags(std::move(result));
}
void RemoveTagFromSelection(
TextWithTags::Tags *tags,
const QString &tag) {
auto result = TextWithTags::Tags();
result.reserve(tags->size());
for (const auto &existing : *tags) {
const auto updated = TextUtilities::TagWithRemoved(existing.id, tag);
if (!updated.isEmpty()) {
result.push_back({
.offset = existing.offset,
.length = existing.length,
.id = updated,
});
}
}
*tags = std::move(result);
}
[[nodiscard]] bool SplitTextSpan(
const TextWithEntities &text,
int from,
int till,
TextWithEntities *before,
TextWithEntities *selected,
TextWithEntities *after) {
if (!before || !selected || !after) {
return false;
}
const auto textSize = int(text.text.size());
from = std::clamp(from, 0, textSize);
till = std::clamp(till, from, textSize);
if (from >= till) {
return false;
}
*before = Ui::Text::Mid(text, 0, from);
*selected = Ui::Text::Mid(text, from, till - from);
if (selected->text.isEmpty()) {
return false;
}
*after = Ui::Text::Mid(text, till);
return true;
}
[[nodiscard]] bool MediaBlockSupportsSpoiler(const Block &block) {
switch (block.kind) {
case BlockKind::Photo:
case BlockKind::Video:
case BlockKind::Audio:
case BlockKind::Map:
return true;
case BlockKind::GroupedMedia:
return ranges::any_of(
block.mediaItems,
[](const RichPage::GroupedMediaItem &item) {
return (item.kind == BlockKind::Photo)
|| (item.kind == BlockKind::Video)
|| (item.kind == BlockKind::Audio)
|| (item.kind == BlockKind::Map);
});
default:
return false;
}
}
[[nodiscard]] bool MediaBlockHasSpoiler(const Block &block) {
if (block.kind == BlockKind::GroupedMedia) {
auto any = false;
for (const auto &item : block.mediaItems) {
if ((item.kind != BlockKind::Photo)
&& (item.kind != BlockKind::Video)
&& (item.kind != BlockKind::Audio)
&& (item.kind != BlockKind::Map)) {
continue;
}
any = true;
if (!item.spoiler) {
return false;
}
}
return any;
}
return block.spoiler;
}
bool SetMediaBlockSpoiler(Block *block, bool enabled) {
if (!block || !MediaBlockSupportsSpoiler(*block)) {
return false;
} else if (block->kind == BlockKind::GroupedMedia) {
auto changed = false;
for (auto &item : block->mediaItems) {
if ((item.kind != BlockKind::Photo)
&& (item.kind != BlockKind::Video)
&& (item.kind != BlockKind::Audio)
&& (item.kind != BlockKind::Map)) {
continue;
}
if (item.spoiler != enabled) {
item.spoiler = enabled;
changed = true;
}
}
return changed;
} else if (block->spoiler != enabled) {
block->spoiler = enabled;
return true;
}
return false;
}
[[nodiscard]] bool IsPhotoVideoBlockKind(BlockKind kind) {
return (kind == BlockKind::Photo) || (kind == BlockKind::Video);
}
[[nodiscard]] bool IsReplaceableMediaBlockKind(BlockKind kind) {
return IsPhotoVideoBlockKind(kind) || (kind == BlockKind::Audio);
}
[[nodiscard]] bool IsTaskList(const std::vector<ListItem> &items) {
return std::any_of(
items.begin(),
items.end(),
[](const ListItem &item) {
return item.taskState != TaskState::None;
});
}
[[nodiscard]] bool IsTaskList(const ClipboardListItemsData &data) {
return data.taskList || IsTaskList(data.items);
}
[[nodiscard]] PreparedOrderedListType ResolvePreparedOrderedListType(
const std::optional<QString> &type) {
if (!type.has_value()) {
return PreparedOrderedListType::Decimal;
}
const auto &value = *type;
if (value == u"a"_q
|| value.compare(u"lower-alpha"_q, Qt::CaseInsensitive) == 0
|| value.compare(u"lower-latin"_q, Qt::CaseInsensitive) == 0) {
return PreparedOrderedListType::LowerAlpha;
} else if (value == u"A"_q
|| value.compare(u"upper-alpha"_q, Qt::CaseInsensitive) == 0
|| value.compare(u"upper-latin"_q, Qt::CaseInsensitive) == 0) {
return PreparedOrderedListType::UpperAlpha;
} else if (value == u"i"_q
|| value.compare(u"lower-roman"_q, Qt::CaseInsensitive) == 0) {
return PreparedOrderedListType::LowerRoman;
} else if (value == u"I"_q
|| value.compare(u"upper-roman"_q, Qt::CaseInsensitive) == 0) {
return PreparedOrderedListType::UpperRoman;
}
return PreparedOrderedListType::Decimal;
}
[[nodiscard]] std::optional<QString> StoredOrderedListType(
PreparedOrderedListType type,
bool explicitDecimal = false) {
switch (type) {
case PreparedOrderedListType::LowerAlpha:
return u"a"_q;
case PreparedOrderedListType::UpperAlpha:
return u"A"_q;
case PreparedOrderedListType::LowerRoman:
return u"i"_q;
case PreparedOrderedListType::UpperRoman:
return u"I"_q;
case PreparedOrderedListType::Decimal:
return explicitDecimal ? std::make_optional(u"1"_q) : std::nullopt;
}
return explicitDecimal ? std::make_optional(u"1"_q) : std::nullopt;
}
[[nodiscard]] int OrderedListSequenceStart(const Block &block) {
return block.orderedList.start.value_or(
block.orderedList.reversed ? int(block.listItems.size()) : 1);
}
[[nodiscard]] int OrderedListSequenceStep(const Block &block) {
return block.orderedList.reversed ? -1 : 1;
}
[[nodiscard]] std::optional<int> EffectiveOrderedItemValue(
const Block &block,
int itemIndex) {
if (block.kind != BlockKind::List
|| block.listKind != ListKind::Ordered
|| itemIndex < 0
|| itemIndex >= int(block.listItems.size())) {
return std::nullopt;
}
auto next = OrderedListSequenceStart(block);
const auto step = OrderedListSequenceStep(block);
for (auto i = 0; i <= itemIndex; ++i) {
const auto value = block.listItems[i].number.value.value_or(next);
if (i == itemIndex) {
return value;
}
next = value + step;
}
return std::nullopt;
}
[[nodiscard]] bool ClearOrderedListRawMarkers(
Block *block,
int from,
int till) {
if (!block
|| block->kind != BlockKind::List
|| block->listKind != ListKind::Ordered) {
return false;
}
auto changed = false;
from = std::clamp(from, 0, int(block->listItems.size()));
till = std::clamp(till, from, int(block->listItems.size()));
for (auto i = from; i != till; ++i) {
auto &item = block->listItems[i];
if (item.number.num.has_value()) {
item.number.num = std::nullopt;
changed = true;
}
}
return changed;
}
[[nodiscard]] bool ClearOrderedListRawMarkers(Block *block) {
return block
? ClearOrderedListRawMarkers(block, 0, int(block->listItems.size()))
: false;
}
[[nodiscard]] bool ClearOrderedTaskStates(Block *block) {
if (!block || block->kind != BlockKind::List) {
return false;
}
auto changed = false;
for (auto &item : block->listItems) {
if (item.taskState != TaskState::None) {
item.taskState = TaskState::None;
changed = true;
}
}
return changed;
}
[[nodiscard]] bool ClearOrderedListData(ListItem *item) {
if (!item) {
return false;
}
const auto changed = item->number.num.has_value()
|| item->number.value.has_value()
|| item->number.type.has_value();
item->number = {};
return changed;
}
[[nodiscard]] bool ResetNonOrderedListMetadata(Block *block) {
if (!block || block->kind != BlockKind::List) {
return false;
}
auto changed = (block->orderedList != OrderedListData());
block->orderedList = {};
for (auto &item : block->listItems) {
changed = ClearOrderedListData(&item) || changed;
}
return changed;
}
void NormalizeInsertedOrderedListMetadata(Block *block) {
if (!block) {
return;
}
for (auto &child : block->blocks) {
NormalizeInsertedOrderedListMetadata(&child);
}
for (auto &item : block->listItems) {
for (auto &child : item.blocks) {
NormalizeInsertedOrderedListMetadata(&child);
}
}
if (block->kind != BlockKind::List) {
return;
}
if (block->listKind != ListKind::Ordered) {
(void)ResetNonOrderedListMetadata(block);
}
}
void NormalizeInsertedOrderedListMetadata(std::vector<Block> *blocks) {
if (!blocks) {
return;
}
for (auto &block : *blocks) {
NormalizeInsertedOrderedListMetadata(&block);
}
}
[[nodiscard]] ListStyle CurrentListStyle(const Block &block) {
return (block.listKind == ListKind::Ordered)
? ListStyle::Ordered
: IsTaskList(block.listItems)
? ListStyle::Task
: ListStyle::Bullet;
}
[[nodiscard]] PreparedOrderedListType EffectiveOrderedListType(
const Block &block,
const ListItem &item) {
return item.number.type.has_value()
? ResolvePreparedOrderedListType(item.number.type)
: ResolvePreparedOrderedListType(block.orderedList.type);
}
[[nodiscard]] bool ListBlockMatchesClipboardData(
const Block &block,
const ClipboardListItemsData &data) {
if (block.kind != BlockKind::List
|| block.listKind != data.listKind
|| IsTaskList(block.listItems) != IsTaskList(data)) {
return false;
}
return (block.listKind != ListKind::Ordered)
|| (block.orderedList == data.orderedList);
}
[[nodiscard]] std::optional<uint64> ReplaceTargetMediaId(const Block &block) {
switch (block.kind) {
case BlockKind::Photo:
return block.photoId ? std::make_optional(block.photoId) : std::nullopt;
case BlockKind::Video:
case BlockKind::Audio:
return block.documentId
? std::make_optional(block.documentId)
: std::nullopt;
default:
return std::nullopt;
}
}
[[nodiscard]] bool BlockMatchesReplaceTarget(
const Block &block,
const ReplaceTarget &target) {
const auto mediaId = ReplaceTargetMediaId(block);
return (block.kind == target.kind)
&& mediaId
&& (*mediaId == target.mediaId);
}
[[nodiscard]] std::optional<RichPage::GroupedMediaItem>
GroupedItemFromPhotoVideoBlock(const Block &block) {
if (!IsPhotoVideoBlockKind(block.kind)) {
return std::nullopt;
}
auto result = RichPage::GroupedMediaItem();
result.kind = block.kind;
result.photo = block.photo;
result.document = block.document;
result.photoId = block.photoId;
result.documentId = block.documentId;
result.width = block.width;
result.height = block.height;
result.autoplay = block.autoplay;
result.loop = block.loop;
result.spoiler = block.spoiler;
return result;
}
[[nodiscard]] std::optional<Block> PhotoVideoBlockFromGroupedItem(
const RichPage::GroupedMediaItem &item) {
if (!IsPhotoVideoBlockKind(item.kind)) {
return std::nullopt;
}
auto result = Block();
result.kind = item.kind;
result.photo = item.photo;
result.document = item.document;
result.photoId = item.photoId;
result.documentId = item.documentId;
result.width = item.width;
result.height = item.height;
result.autoplay = item.autoplay;
result.loop = item.loop;
result.spoiler = item.spoiler;
return result;
}
[[nodiscard]] bool GroupingRichTextIsEmpty(const RichText &text) {
return text.text.text.trimmed().isEmpty()
&& text.anchorId.isEmpty()
&& text.anchorIds.empty();
}
[[nodiscard]] bool BlockHasGroupingCaptionOrAnchor(const Block &block) {
return !GroupingRichTextIsEmpty(block.caption)
|| !block.anchorId.isEmpty();
}
[[nodiscard]] bool HasValidGroupingCaptionAndAnchorSource(
const std::vector<Block> &blocks,
int from,
int till) {
auto found = false;
for (auto i = from; i != till; ++i) {
if (!BlockHasGroupingCaptionOrAnchor(blocks[i])) {
continue;
} else if (found) {
return false;
}
found = true;
}
return true;
}
[[nodiscard]] BlockContainerPath BlockChildrenContainer(BlockPath path) {
auto result = std::move(path.container);
result.steps.push_back({
.kind = BlockContainerKind::BlockChildren,
.blockIndex = path.index,
});
return result;
}
[[nodiscard]] BlockContainerPath ListItemChildrenContainer(
BlockPath path,
int itemIndex) {
auto result = std::move(path.container);
result.steps.push_back({
.kind = BlockContainerKind::ListItemChildren,
.blockIndex = path.index,
.listItemIndex = itemIndex,
});
return result;
}
[[nodiscard]] bool ContainerStartsWith(
const BlockContainerPath &container,
const BlockContainerPath &prefix) {
if (container.steps.size() < prefix.steps.size()) {
return false;
}
for (auto i = 0, count = int(prefix.steps.size()); i != count; ++i) {
const auto &a = container.steps[i];
const auto &b = prefix.steps[i];
if (a.kind != b.kind
|| a.blockIndex != b.blockIndex
|| a.listItemIndex != b.listItemIndex) {
return false;
}
}
return true;
}
[[nodiscard]] PreparedBlockContainerPath ToPreparedBlockContainerPath(
const BlockContainerPath &path) {
auto result = PreparedBlockContainerPath();
result.steps.reserve(path.steps.size());
for (const auto &step : path.steps) {
auto converted = PreparedBlockContainerStep();
converted.blockIndex = step.blockIndex;
converted.listItemIndex = step.listItemIndex;
switch (step.kind) {
case BlockContainerKind::Root:
continue;
case BlockContainerKind::BlockChildren:
converted.kind = PreparedBlockContainerKind::BlockChildren;
break;
case BlockContainerKind::ListItemChildren:
converted.kind = PreparedBlockContainerKind::ListItemChildren;
break;
}
result.steps.push_back(converted);
}
return result;
}
[[nodiscard]] bool ContainerHasPrefix(
const BlockContainerPath &path,
const BlockContainerPath &prefix) {
if (path.steps.size() < prefix.steps.size()) {
return false;
}
return std::equal(
prefix.steps.begin(),
prefix.steps.end(),
path.steps.begin());
}
[[nodiscard]] bool IndexInRange(int index, int from, int till) {
return (index >= from) && (index < till);
}
[[nodiscard]] bool ShiftBlockContainerPathAfterRemovedBlock(
BlockContainerPath &path,
const BlockPath &removed) {
if (!ContainerHasPrefix(path, removed.container)) {
return true;
}
const auto size = removed.container.steps.size();
if (path.steps.size() == size) {
return true;
}
auto &step = path.steps[size];
if (step.blockIndex == removed.index) {
return false;
} else if (step.blockIndex > removed.index) {
--step.blockIndex;
}
return true;
}
[[nodiscard]] bool ShiftBlockPathAfterRemovedBlock(
BlockPath &path,
const BlockPath &removed) {
if (path.container == removed.container) {
if (path.index == removed.index) {
return false;
} else if (path.index > removed.index) {
--path.index;
}
return true;
}
return ShiftBlockContainerPathAfterRemovedBlock(path.container, removed);
}
[[nodiscard]] bool ShiftBlockContainerPathAfterRemovedListItem(
BlockContainerPath &path,
const BlockPath &list,
int removedItemIndex) {
const auto removed = ListItemChildrenContainer(list, removedItemIndex);
if (ContainerHasPrefix(path, removed)) {
return false;
}
if (!ContainerHasPrefix(path, list.container)) {
return true;
}
const auto size = list.container.steps.size();
if (path.steps.size() <= size) {
return true;
}
auto &step = path.steps[size];
if (step.blockIndex != list.index
|| step.kind != BlockContainerKind::ListItemChildren) {
return true;
}
if (step.listItemIndex == removedItemIndex) {
return false;
} else if (step.listItemIndex > removedItemIndex) {
--step.listItemIndex;
}
return true;
}
[[nodiscard]] bool ShiftBlockPathAfterRemovedListItem(
BlockPath &path,
const BlockPath &list,
int removedItemIndex) {
return ShiftBlockContainerPathAfterRemovedListItem(
path.container,
list,
removedItemIndex);
}
[[nodiscard]] std::optional<int> BlockIndexInContainer(
const LeafPath &leaf,
const BlockContainerPath &container) {
if (leaf.block.container == container) {
return leaf.block.index;
}
if (!ContainerHasPrefix(leaf.block.container, container)
|| leaf.block.container.steps.size() <= container.steps.size()) {
return std::nullopt;
}
const auto &step = leaf.block.container.steps[container.steps.size()];
return (step.kind == BlockContainerKind::BlockChildren
|| step.kind == BlockContainerKind::ListItemChildren)
? std::make_optional(step.blockIndex)
: std::nullopt;
}
[[nodiscard]] std::optional<int> ListItemIndexForLeaf(
const LeafPath &leaf,
const BlockPath &block) {
if (leaf.block == block && leaf.kind == LeafKind::ListItemText) {
return leaf.listItemIndex;
}
if (!ContainerHasPrefix(leaf.block.container, block.container)
|| leaf.block.container.steps.size() <= block.container.steps.size()) {
return std::nullopt;
}
const auto &step = leaf.block.container.steps[block.container.steps.size()];
return (step.kind == BlockContainerKind::ListItemChildren
&& step.blockIndex == block.index)
? std::make_optional(step.listItemIndex)
: std::nullopt;
}
[[nodiscard]] std::optional<int> TableRowIndexForLeaf(
const LeafPath &leaf,
const BlockPath &block) {
if (!(leaf.block == block)) {
return std::nullopt;
}
if (leaf.kind == LeafKind::BlockText) {
return -1;
}
return (leaf.kind == LeafKind::TableCellText)
? std::make_optional(leaf.tableRowIndex)
: std::nullopt;
}
[[nodiscard]] std::optional<int> TableCellIndexForLeaf(
const LeafPath &leaf,
const BlockPath &block,
int rowIndex) {
return (leaf.block == block
&& leaf.kind == LeafKind::TableCellText
&& leaf.tableRowIndex == rowIndex)
? std::make_optional(leaf.tableCellIndex)
: std::nullopt;
}
using TableGridOccupancyRow = std::vector<char>;
using TableGridOccupancy = std::vector<TableGridOccupancyRow>;
struct TableGridCellReference {
int rowIndex = -1;
int cellIndex = -1;
int rowFrom = -1;
int rowTill = -1;
int columnFrom = -1;
int columnTill = -1;
};
struct TableGrid {
std::vector<TableGridCellReference> cells;
TableGridOccupancy occupancy;
int rowCount = 0;
int columnCount = 0;
};
[[nodiscard]] int NormalizeTableSpan(int span) {
return std::max(span, 1);
}
[[nodiscard]] int ClampTableRowspan(
int rawRowspan,
int row,
int rowCount) {
if ((row < 0) || (row >= rowCount) || (rowCount <= 0)) {
return 0;
}
const auto remainingRows = int64(rowCount) - row;
return int(std::min<int64>(NormalizeTableSpan(rawRowspan), remainingRows));
}
[[nodiscard]] int ClampTableColspan(
int rawColspan,
int column,
int maxColumns) {
if ((column < 0) || (column >= maxColumns) || (maxColumns <= 0)) {
return 0;
}
const auto remainingColumns = int64(maxColumns) - column;
return int(std::min<int64>(
NormalizeTableSpan(rawColspan),
remainingColumns));
}
[[nodiscard]] bool CanOccupyTableSlots(
const TableGridOccupancy &occupancy,
int row,
int column,
int rowspan,
int colspan) {
if ((row < 0)
|| (column < 0)
|| (rowspan <= 0)
|| (colspan <= 0)
|| (row >= int(occupancy.size()))) {
return false;
}
const auto rowLimit = int(std::min<int64>(
int64(row) + rowspan,
occupancy.size()));
const auto columnLimit64 = int64(column) + colspan;
if (columnLimit64 <= column) {
return false;
}
const auto columnLimit = int(std::min<int64>(
columnLimit64,
std::numeric_limits<int>::max()));
for (auto currentRow = row; currentRow < rowLimit; ++currentRow) {
const auto &occupied = occupancy[currentRow];
const auto occupiedLimit = std::min(columnLimit, int(occupied.size()));
for (auto currentColumn = column;
currentColumn < occupiedLimit;
++currentColumn) {
if (occupied[currentColumn]) {
return false;
}
}
}
return true;
}
[[nodiscard]] int FirstAvailableTableColumn(
const TableGridOccupancy &occupancy,
int row,
int rowspan,
int colspan,
int maxColumns) {
if ((row < 0)
|| (row >= int(occupancy.size()))
|| (rowspan <= 0)
|| (colspan <= 0)
|| (maxColumns <= 0)) {
return -1;
}
for (auto column = 0; column < maxColumns; ++column) {
const auto effectiveColspan = ClampTableColspan(
colspan,
column,
maxColumns);
if (effectiveColspan <= 0) {
continue;
}
if (CanOccupyTableSlots(
occupancy,
row,
column,
rowspan,
effectiveColspan)) {
return column;
}
}
return -1;
}
void MarkTableSlots(
TableGridOccupancy *occupancy,
int row,
int column,
int rowspan,
int colspan) {
if ((row < 0)
|| (column < 0)
|| (rowspan <= 0)
|| (colspan <= 0)
|| (row >= int(occupancy->size()))) {
return;
}
const auto rowLimit = int(std::min<int64>(
int64(row) + rowspan,
occupancy->size()));
const auto columnLimit64 = int64(column) + colspan;
if (columnLimit64 <= column) {
return;
}
const auto columnLimit = int(std::min<int64>(
columnLimit64,
std::numeric_limits<int>::max()));
for (auto currentRow = row; currentRow < rowLimit; ++currentRow) {
auto &occupied = (*occupancy)[currentRow];
if (columnLimit > int(occupied.size())) {
occupied.resize(columnLimit, false);
}
for (auto currentColumn = column;
currentColumn < columnLimit;
++currentColumn) {
occupied[currentColumn] = true;
}
}
}
[[nodiscard]] int TableGridColumnCount(
const TableGridOccupancy &occupancy) {
auto result = 0;
for (const auto &row : occupancy) {
result = std::max(result, int(row.size()));
}
return result;
}
[[nodiscard]] TableGrid BuildTableGrid(
const Block &table,
const Markdown::MarkdownPrepareTableRenderLimits &limits) {
auto result = TableGrid();
result.rowCount = std::min(int(table.tableRows.size()), limits.maxRows);
if (result.rowCount < 0) {
result.rowCount = 0;
}
result.occupancy = TableGridOccupancy(result.rowCount);
auto occupiedSlotCountSoFar = int64(0);
for (auto rowIndex = 0; rowIndex != result.rowCount; ++rowIndex) {
const auto &row = table.tableRows[rowIndex];
for (auto cellIndex = 0, cellCount = int(row.cells.size());
cellIndex != cellCount;
++cellIndex) {
const auto &cell = row.cells[cellIndex];
const auto normalizedColspan = NormalizeTableSpan(cell.colspan);
const auto rowspan = ClampTableRowspan(
cell.rowspan,
rowIndex,
result.rowCount);
if (rowspan <= 0) {
continue;
}
const auto column = FirstAvailableTableColumn(
result.occupancy,
rowIndex,
rowspan,
normalizedColspan,
limits.maxColumns);
if (column < 0) {
continue;
}
const auto colspan = ClampTableColspan(
normalizedColspan,
column,
limits.maxColumns);
if (colspan <= 0) {
continue;
}
const auto occupiedSlotGrowth = int64(rowspan) * colspan;
if (occupiedSlotGrowth > limits.maxCells
|| (occupiedSlotCountSoFar + occupiedSlotGrowth)
> limits.maxCells) {
continue;
}
result.cells.push_back({
.rowIndex = rowIndex,
.cellIndex = cellIndex,
.rowFrom = rowIndex,
.rowTill = rowIndex + rowspan,
.columnFrom = column,
.columnTill = column + colspan,
});
MarkTableSlots(
&result.occupancy,
rowIndex,
column,
rowspan,
colspan);
occupiedSlotCountSoFar += occupiedSlotGrowth;
}
}
result.columnCount = TableGridColumnCount(result.occupancy);
return result;
}
template <typename Range>
[[nodiscard]] bool TableGridCellIntersectsRange(
const TableGridCellReference &cell,
const Range &range) {
return (cell.rowFrom < range.rowTill)
&& (cell.rowTill > range.rowFrom)
&& (cell.columnFrom < range.columnTill)
&& (cell.columnTill > range.columnFrom);
}
template <typename Range>
[[nodiscard]] bool TableGridCellContainedInRange(
const TableGridCellReference &cell,
const Range &range) {
return (cell.rowFrom >= range.rowFrom)
&& (cell.rowTill <= range.rowTill)
&& (cell.columnFrom >= range.columnFrom)
&& (cell.columnTill <= range.columnTill);
}
template <typename Range>
[[nodiscard]] std::vector<TableGridCellReference> SelectedTableGridCells(
const TableGrid &grid,
const Range &range) {
auto result = std::vector<TableGridCellReference>();
result.reserve(grid.cells.size());
for (const auto &cell : grid.cells) {
if (TableGridCellIntersectsRange(cell, range)) {
result.push_back(cell);
}
}
return result;
}
template <typename Range>
[[nodiscard]] bool TableGridRangeCovered(
const TableGrid &grid,
const Range &range) {
if ((range.rowFrom < 0)
|| (range.rowTill <= range.rowFrom)
|| (range.columnFrom < 0)
|| (range.columnTill <= range.columnFrom)
|| (range.rowTill > grid.rowCount)
|| (range.columnTill > grid.columnCount)) {
return false;
}
for (auto row = range.rowFrom; row != range.rowTill; ++row) {
if (row >= int(grid.occupancy.size())) {
return false;
}
const auto &occupied = grid.occupancy[row];
for (auto column = range.columnFrom;
column != range.columnTill;
++column) {
if (column >= int(occupied.size()) || !occupied[column]) {
return false;
}
}
}
return true;
}
template <typename Range>
[[maybe_unused]] [[nodiscard]] bool CleanTableGridUniteRange(
const TableGrid &grid,
const Range &range) {
const auto selected = SelectedTableGridCells(grid, range);
if (selected.empty()) {
return false;
}
for (const auto &cell : selected) {
if (!TableGridCellContainedInRange(cell, range)) {
return false;
}
}
return TableGridRangeCovered(grid, range);
}
template <typename Range>
[[nodiscard]] bool TableGridRangeSpansAllRows(
const TableGrid &grid,
const Range &range) {
return (range.rowFrom == 0)
&& (range.rowTill == grid.rowCount)
&& (range.columnFrom >= 0)
&& (range.columnTill > range.columnFrom)
&& (range.columnTill <= grid.columnCount);
}
template <typename Range>
[[nodiscard]] bool TableGridRangeSpansAllColumns(
const TableGrid &grid,
const Range &range) {
return (range.rowFrom >= 0)
&& (range.rowTill > range.rowFrom)
&& (range.rowTill <= grid.rowCount)
&& (range.columnFrom == 0)
&& (range.columnTill == grid.columnCount);
}
template <typename Range>
[[nodiscard]] bool TableGridRangeCoversFullTable(
const TableGrid &grid,
const Range &range) {
return TableGridRangeSpansAllRows(grid, range)
&& TableGridRangeSpansAllColumns(grid, range);
}
template <typename Range>
[[nodiscard]] int TableGridCellColumnIntersection(
const TableGridCellReference &cell,
const Range &range) {
const auto from = std::max(cell.columnFrom, range.columnFrom);
const auto till = std::min(cell.columnTill, range.columnTill);
return std::max(till - from, 0);
}
[[nodiscard]] bool TableGridCellMatchesLeaf(
const TableGridCellReference &cell,
const LeafPath &leaf,
const BlockPath &block) {
const auto index = TableCellIndexForLeaf(leaf, block, cell.rowIndex);
return index && *index == cell.cellIndex;
}
[[nodiscard]] TableCell MakeDefaultTableCell() {
return TableCell();
}
[[nodiscard]] TableCell MakeDefaultTableCell(bool header) {
auto result = MakeDefaultTableCell();
result.header = header;
return result;
}
[[nodiscard]] const TableCell *TableGridCellAt(
const Block &table,
const TableGrid &grid,
int row,
int column) {
if (row < 0 || row >= grid.rowCount || column < 0) {
return nullptr;
}
for (const auto &reference : grid.cells) {
if (reference.rowFrom <= row
&& reference.rowTill > row
&& reference.columnFrom <= column
&& reference.columnTill > column) {
return &table.tableRows[reference.rowIndex].cells[
reference.cellIndex];
}
}
return nullptr;
}
[[nodiscard]] int IncrementTableSpan(int span) {
const auto normalized = NormalizeTableSpan(span);
return (normalized == std::numeric_limits<int>::max())
? normalized
: normalized + 1;
}
void InsertTableCellBeforeVisualColumn(
TableRow *row,
const TableGrid &grid,
int rowIndex,
int column,
TableCell insertedCell) {
auto insertAt = int(row->cells.size());
for (const auto &reference : grid.cells) {
if (reference.rowIndex == rowIndex
&& reference.columnFrom >= column) {
insertAt = std::min(reference.cellIndex, int(row->cells.size()));
break;
}
}
row->cells.insert(
row->cells.begin() + insertAt,
std::move(insertedCell));
}
[[nodiscard]] bool BlockCanOwnChildContainer(const Block &block) {
return (block.kind == BlockKind::Quote)
|| (block.kind == BlockKind::Details);
}
[[nodiscard]] bool BlockSupportsBlockText(const Block &block) {
switch (block.kind) {
case BlockKind::Heading:
case BlockKind::Paragraph:
case BlockKind::Footer:
case BlockKind::Code:
case BlockKind::Quote:
case BlockKind::Table:
case BlockKind::Details:
return true;
default:
return false;
}
}
[[nodiscard]] bool BlockSupportsBlockCaption(const Block &block) {
switch (block.kind) {
case BlockKind::Quote:
case BlockKind::Photo:
case BlockKind::Video:
case BlockKind::Audio:
case BlockKind::Map:
return true;
default:
return false;
}
}
[[nodiscard]] bool StringIsEmpty(const QString &text) {
return text.trimmed().isEmpty();
}
[[nodiscard]] bool RichTextHasVisibleText(const RichText &text) {
return !StringIsEmpty(text.text.text);
}
void MergeRichTextAnchors(RichText *target, RichText source) {
if (!target) {
return;
}
if (!source.anchorId.isEmpty()) {
if (target->anchorId.isEmpty()) {
target->anchorId = std::move(source.anchorId);
} else {
target->anchorIds.push_back(std::move(source.anchorId));
}
}
for (auto &anchorId : source.anchorIds) {
if (!anchorId.isEmpty()) {
target->anchorIds.push_back(std::move(anchorId));
}
}
}
[[nodiscard]] bool CanEditBlocks(const std::vector<Block> &blocks);
[[nodiscard]] bool CanEditBlock(const Block &block) {
switch (block.kind) {
case BlockKind::Heading:
case BlockKind::Paragraph:
case BlockKind::Footer:
case BlockKind::Code:
case BlockKind::Divider:
case BlockKind::Anchor:
case BlockKind::GroupedMedia:
case BlockKind::Photo:
case BlockKind::Video:
case BlockKind::Audio:
case BlockKind::Math:
case BlockKind::Table:
case BlockKind::Map:
return true;
case BlockKind::Quote:
case BlockKind::Details:
return CanEditBlocks(block.blocks);
case BlockKind::List:
return ranges::all_of(block.listItems, [](const ListItem &item) {
return CanEditBlocks(item.blocks);
});
case BlockKind::Unsupported:
case BlockKind::Thinking:
case BlockKind::AuthorDate:
case BlockKind::Embed:
case BlockKind::EmbedPost:
case BlockKind::Channel:
case BlockKind::RelatedArticles:
return false;
}
return false;
}
[[nodiscard]] bool CanEditBlocks(const std::vector<Block> &blocks) {
return ranges::all_of(blocks, &CanEditBlock);
}
} // namespace
State::State()
: State(std::make_shared<RichPage>(), nullptr, RichMessageLimits()) {
}
State::State(
std::shared_ptr<RichPage> richPage,
std::shared_ptr<Markdown::MediaRuntime> mediaRuntime,
RichMessageLimits limits)
: _richPage(richPage ? std::move(richPage) : std::make_shared<RichPage>())
, _mediaRuntime(std::move(mediaRuntime))
, _limits(std::move(limits)) {
if (_richPage->blocks.empty()) {
_richPage->blocks.push_back(MakeParagraphBlock());
}
rebuild();
}
const RichPage &State::richPage() const {
return *_richPage;
}
bool State::articleEmpty() const {
return ranges::all_of(_richPage->blocks, [](const auto &block) {
return BlockIsEmpty(block);
});
}
const Markdown::MarkdownArticleContent &State::prepared() const {
return _prepared;
}
auto State::tableRenderLimits() const
-> Markdown::MarkdownPrepareTableRenderLimits {
return Markdown::PrepareTableRenderLimitsForRichMessage(_limits);
}
template <typename Result, typename Callback>
Result State::applyCheckedMutation(Result failure, Callback &&callback) {
_lastLimitError = std::nullopt;
auto candidate = State(
std::make_shared<RichPage>(*_richPage),
_mediaRuntime,
_limits);
candidate._activeTextOrdinal = _activeTextOrdinal;
candidate._lastLimitError = std::nullopt;
candidate._temporaryDownParagraph = _temporaryDownParagraph;
const auto outcome = callback(candidate);
if (!outcome.apply) {
return outcome.result;
}
if (const auto error = ValidateRichMessage(*candidate._richPage, _limits)) {
_lastLimitError = error;
return failure;
}
commitCheckedMutation(std::move(candidate));
return outcome.result;
}
void State::commitCheckedMutation(State state) {
_richPage = std::move(state._richPage);
_prepared = std::move(state._prepared);
_textNodes = std::move(state._textNodes);
_activeTextOrdinal = state._activeTextOrdinal;
_lastPreparedMutationKind = (
state._lastPreparedMutationKind == PreparedMutationKind::FullRebuild)
? state._lastPreparedMutationKind
: PreparedMutationKind::FullRebuild;
_lastLimitError = std::nullopt;
_temporaryDownParagraph = std::move(state._temporaryDownParagraph);
}
const std::vector<TextNodeDescriptor> &State::textNodes() const {
return _textNodes;
}
State::Snapshot State::snapshot() const {
return {
.richPage = *_richPage,
.activeLeaf = activeLeafPath(),
.temporaryDownParagraph = _temporaryDownParagraph,
};
}
void State::restoreSnapshot(Snapshot snapshot) {
_richPage = std::make_shared<RichPage>(std::move(snapshot.richPage));
_activeTextOrdinal = -1;
_lastLimitError = std::nullopt;
_temporaryDownParagraph = std::move(snapshot.temporaryDownParagraph);
rebuild();
if (snapshot.activeLeaf && (textNodeOrdinal(*snapshot.activeLeaf) >= 0)) {
const auto activated = activateRebuiltLeaf(*snapshot.activeLeaf);
Assert(activated);
} else {
ensureActiveTextOrdinal();
}
}
std::optional<LeafPath> State::activeLeafPath() const {
if (const auto descriptor = textNode(_activeTextOrdinal)) {
return descriptor->leaf;
}
return std::nullopt;
}
int State::textOrdinalForLeafPath(const LeafPath &path) const {
return textNodeOrdinal(path);
}
void State::clearTemporaryDownParagraph() {
_temporaryDownParagraph = std::nullopt;
}
void State::clearTemporaryDownParagraphIfInvalid() {
if (!_temporaryDownParagraph
|| _temporaryDownParagraph->kind != LeafKind::BlockText
|| (textNodeOrdinal(*_temporaryDownParagraph) < 0)) {
clearTemporaryDownParagraph();
return;
}
const auto owner = block(_temporaryDownParagraph->block);
if (!owner
|| owner->kind != BlockKind::Paragraph
|| !BlockIsEmpty(*owner)) {
clearTemporaryDownParagraph();
}
}
int State::textOrdinalForLeaf(
const Markdown::PreparedEditLeafSource &source) const {
const auto leaf = convertLeafPath(source);
return leaf ? textOrdinalForLeafPath(*leaf) : -1;
}
std::optional<PreparedEditLeafSource> State::preparedLeafSourceForOrdinal(
int ordinal) const {
const auto descriptor = textNode(ordinal);
return descriptor ? convertPreparedLeafSource(*descriptor) : std::nullopt;
}
PreparedMutationKind State::lastPreparedMutationKind() const {
return _lastPreparedMutationKind;
}
std::optional<PreparedEditLeafSource> State::activePreparedLeafSource() const {
const auto descriptor = textNode(_activeTextOrdinal);
return descriptor ? convertPreparedLeafSource(*descriptor) : std::nullopt;
}
std::vector<TextNodeSpan> State::resolveTextSpansForPreparedLeafRange(
const PreparedEditLeafSource &source,
int from,
int till) const {
if (from < 0 || till <= from) {
return {};
}
const auto firstLeaf = convertLeafPath(source);
if (!firstLeaf) {
return {};
}
const auto firstOrdinal = textOrdinalForLeafPath(*firstLeaf);
if (firstOrdinal < 0) {
return {};
}
auto result = std::vector<TextNodeSpan>();
auto consumed = 0;
for (auto i = firstOrdinal, count = textNodeCount()
; i != count && consumed < till
; ++i) {
const auto current = richText(_textNodes[i].leaf);
if (!current) {
return {};
}
const auto length = int(current->text.text.size());
const auto spanFrom = std::max(from - consumed, 0);
const auto spanTo = std::min(till - consumed, length);
if (spanFrom < spanTo) {
result.push_back(TextNodeSpan{
.leaf = _textNodes[i].leaf,
.from = spanFrom,
.till = spanTo,
});
}
consumed += length;
}
return (consumed >= till) ? result : std::vector<TextNodeSpan>();
}
int State::textNodeCount() const {
return int(_textNodes.size());
}
int State::activeTextOrdinal() const {
return _activeTextOrdinal;
}
bool State::setActiveTextByOrdinal(int ordinal) {
if (ordinal < 0 || ordinal >= textNodeCount()) {
return false;
}
if (_temporaryDownParagraph
&& !(_textNodes[ordinal].leaf == *_temporaryDownParagraph)) {
clearTemporaryDownParagraph();
}
_activeTextOrdinal = ordinal;
return true;
}
TextWithEntities State::activeText() const {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor) {
return TextWithEntities();
}
if (const auto current = richText(descriptor->leaf)) {
return StripEditModeWrapperEntities(current->text);
}
if (const auto current = rawText(descriptor->leaf)) {
return MakeText(*current);
}
return TextWithEntities();
}
ApplyResult State::applyActiveText(TextWithEntities text) {
_lastLimitError = std::nullopt;
_lastPreparedMutationKind = PreparedMutationKind::None;
return applyActiveTextWithLocalLimit(std::move(text));
}
ApplyResult State::applyActiveTextUnchecked(TextWithEntities text) {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor) {
return ApplyResult::Failed;
}
if (auto current = richText(descriptor->leaf)) {
if (current->text == text) {
return ApplyResult::Unchanged;
}
current->text = std::move(text);
if (leafMutationKeepsTextNodes(*descriptor)) {
if (_temporaryDownParagraph
&& (descriptor->leaf == *_temporaryDownParagraph)
&& !RichTextIsEmpty(*current)) {
clearTemporaryDownParagraph();
}
if (updatePreparedActiveLeaf(*descriptor)) {
_lastPreparedMutationKind = PreparedMutationKind::LeafOnly;
} else {
rebuildPrepared();
}
} else {
rebuild();
}
return ApplyResult::Changed;
}
if (auto current = rawText(descriptor->leaf)) {
if (*current == text.text) {
return ApplyResult::Unchanged;
}
*current = std::move(text.text);
if (leafMutationKeepsTextNodes(*descriptor)) {
if (updatePreparedActiveLeaf(*descriptor)) {
_lastPreparedMutationKind = PreparedMutationKind::LeafOnly;
} else {
rebuildPrepared();
}
} else {
rebuild();
}
return ApplyResult::Changed;
}
return ApplyResult::Failed;
}
ApplyResult State::applyActiveTextWithLocalLimit(TextWithEntities text) {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor) {
return ApplyResult::Failed;
}
auto chunks = SplitFieldText(std::move(text));
if (chunks.size() <= 1) {
return applyActiveTextUnchecked(chunks.empty()
? TextWithEntities()
: std::move(chunks.front()));
}
auto first = chunks.front();
if (const auto result = applySplitParagraphText(
*descriptor,
std::move(chunks)); result != ApplyResult::Failed) {
return result;
}
return applyActiveTextUnchecked(std::move(first));
}
FieldMode State::activeFieldMode() const {
const auto descriptor = textNode(_activeTextOrdinal);
return descriptor ? descriptor->mode : FieldMode::Rich;
}
QString State::activeRawText() const {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor) {
return QString();
}
if (const auto current = rawText(descriptor->leaf)) {
return *current;
}
if (const auto current = richText(descriptor->leaf)) {
return StripEditModeWrapperEntities(current->text).text;
}
return QString();
}
QString State::activePlaceholderText() const {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor) {
return QString();
}
const auto owner = block(descriptor->leaf.block);
if (!owner) {
return QString();
}
switch (descriptor->leaf.kind) {
case LeafKind::BlockText:
switch (owner->kind) {
case BlockKind::Paragraph:
case BlockKind::Footer:
case BlockKind::Code:
return u"Text"_q;
case BlockKind::Quote:
return u"Enter quote"_q;
case BlockKind::Heading:
case BlockKind::Details:
return u"Header"_q;
default:
return QString();
}
case LeafKind::BlockCaption:
switch (owner->kind) {
case BlockKind::Quote:
return u"Add author"_q;
case BlockKind::Photo:
case BlockKind::Video:
case BlockKind::Audio:
case BlockKind::Map:
return u"Caption"_q;
default:
return QString();
}
case LeafKind::ListItemText:
return u"Text"_q;
case LeafKind::TableCellText:
return u"Cell"_q;
case LeafKind::MathFormula:
return u"x^2 + y^2"_q;
}
return QString();
}
ApplyResult State::applyActiveRawText(QString text) {
_lastLimitError = std::nullopt;
_lastPreparedMutationKind = PreparedMutationKind::None;
return applyActiveRawTextWithLocalLimit(std::move(text));
}
ApplyResult State::applyActiveRawTextUnchecked(QString text) {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor) {
return ApplyResult::Failed;
}
if (auto current = rawText(descriptor->leaf)) {
if (*current == text) {
return ApplyResult::Unchanged;
}
*current = std::move(text);
if (leafMutationKeepsTextNodes(*descriptor)) {
if (updatePreparedActiveLeaf(*descriptor)) {
_lastPreparedMutationKind = PreparedMutationKind::LeafOnly;
} else {
rebuildPrepared();
}
} else {
rebuild();
}
return ApplyResult::Changed;
}
if (auto current = richText(descriptor->leaf)) {
auto updated = MakeText(std::move(text));
if (current->text == updated) {
return ApplyResult::Unchanged;
}
current->text = std::move(updated);
if (leafMutationKeepsTextNodes(*descriptor)) {
if (_temporaryDownParagraph
&& (descriptor->leaf == *_temporaryDownParagraph)
&& !RichTextIsEmpty(*current)) {
clearTemporaryDownParagraph();
}
if (updatePreparedActiveLeaf(*descriptor)) {
_lastPreparedMutationKind = PreparedMutationKind::LeafOnly;
} else {
rebuildPrepared();
}
} else {
rebuild();
}
return ApplyResult::Changed;
}
return ApplyResult::Failed;
}
ApplyResult State::applyActiveRawTextWithLocalLimit(QString text) {
auto chunks = SplitFieldText(MakeText(std::move(text)));
return applyActiveRawTextUnchecked(chunks.empty()
? QString()
: std::move(chunks.front().text));
}
State::ApplyResult State::applyFormattingToTextSpans(
const std::vector<TextNodeSpan> &spans,
TextFormattingAction action,
std::optional<bool> enabled) {
if (spans.empty()) {
return ApplyResult::Unchanged;
}
return applyCheckedMutation(ApplyResult::Failed, [
spans,
action,
enabled
](State &candidate) {
const auto tag = FormattingActionTag(action);
const auto shouldEnable = enabled.value_or([&] {
if (!tag) {
return false;
}
auto any = false;
for (const auto &span : spans) {
const auto current = candidate.richText(span.leaf);
if (!current) {
continue;
}
auto before = TextWithEntities();
auto selected = TextWithEntities();
auto after = TextWithEntities();
if (!SplitTextSpan(
current->text,
span.from,
span.till,
&before,
&selected,
&after)) {
continue;
}
any = true;
if (!HasFullTextTag(
ConvertRichTextToEditorTags(std::move(selected)).text,
*tag)) {
return true;
}
}
return any ? false : true;
}());
auto changed = false;
for (const auto &span : spans) {
const auto current = candidate.richText(span.leaf);
if (!current) {
continue;
}
auto before = TextWithEntities();
auto selected = TextWithEntities();
auto after = TextWithEntities();
if (!SplitTextSpan(
current->text,
span.from,
span.till,
&before,
&selected,
&after)) {
continue;
}
auto converted = ConvertRichTextToEditorTags(std::move(selected));
if (action == TextFormattingAction::PlainText) {
converted.text.tags.clear();
} else if (tag) {
if (shouldEnable) {
OverlayTag(
&converted.text.tags,
{
.offset = 0,
.length = int(converted.text.text.size()),
.id = *tag,
},
converted.text.text);
} else {
RemoveTagFromSelection(&converted.text.tags, *tag);
}
}
auto demotedHeading = false;
if (action == TextFormattingAction::PlainText
&& span.leaf.kind == LeafKind::BlockText
&& before.text.isEmpty()
&& after.text.isEmpty()) {
if (const auto owner = candidate.block(span.leaf.block);
owner && owner->kind == BlockKind::Heading) {
owner->kind = BlockKind::Paragraph;
owner->headingLevel = 0;
demotedHeading = true;
}
}
auto updated = JoinText(
std::move(before),
ConvertEditorTagsToRichText(std::move(converted.text)),
std::move(after));
if (current->text != updated) {
current->text = std::move(updated);
changed = true;
}
if (demotedHeading) {
changed = true;
}
}
if (!changed) {
return CheckedMutationResult<ApplyResult>{
.result = ApplyResult::Unchanged,
};
}
candidate.rebuild();
return CheckedMutationResult<ApplyResult>{
.apply = true,
.result = ApplyResult::Changed,
};
});
}
bool State::toggleSpoilerOnBlocks(
const std::vector<BlockPath> &blocks,
std::optional<bool> enabled) {
if (blocks.empty()) {
return false;
}
return applyCheckedMutation(false, [blocks, enabled](State &candidate) {
const auto shouldEnable = enabled.value_or([&] {
auto any = false;
for (const auto &path : blocks) {
const auto current = candidate.block(path);
if (!current || !MediaBlockSupportsSpoiler(*current)) {
continue;
}
any = true;
if (!MediaBlockHasSpoiler(*current)) {
return true;
}
}
return any ? false : true;
}());
auto changed = false;
for (const auto &path : blocks) {
const auto current = candidate.block(path);
changed |= SetMediaBlockSpoiler(current, shouldEnable);
}
if (!changed) {
return CheckedMutationResult<bool>{ .result = false };
}
candidate.rebuild();
return CheckedMutationResult<bool>{
.apply = true,
.result = true,
};
});
}
bool State::toggleSpoilerOnGroupedItem(
const BlockPath &path,
int itemIndex,
std::optional<bool> enabled) {
if (itemIndex < 0) {
return false;
}
return applyCheckedMutation(false, [path, itemIndex, enabled](
State &candidate) {
const auto current = candidate.block(path);
if (!current
|| current->kind != BlockKind::GroupedMedia
|| itemIndex >= int(current->mediaItems.size())) {
return CheckedMutationResult<bool>{ .result = false };
}
auto &item = current->mediaItems[itemIndex];
if ((item.kind != BlockKind::Photo)
&& (item.kind != BlockKind::Video)
&& (item.kind != BlockKind::Audio)
&& (item.kind != BlockKind::Map)) {
return CheckedMutationResult<bool>{ .result = false };
}
const auto shouldEnable = enabled.value_or(!item.spoiler);
if (item.spoiler == shouldEnable) {
return CheckedMutationResult<bool>{ .result = false };
}
item.spoiler = shouldEnable;
candidate.rebuild();
return CheckedMutationResult<bool>{
.apply = true,
.result = true,
};
});
}
std::optional<State::ReplaceTarget> State::replaceTargetForBlock(
const BlockPath &path) const {
const auto current = block(path);
if (!current || !IsReplaceableMediaBlockKind(current->kind)) {
return std::nullopt;
}
const auto mediaId = ReplaceTargetMediaId(*current);
if (!mediaId) {
return std::nullopt;
}
return ReplaceTarget{
.path = path,
.kind = current->kind,
.mediaId = *mediaId,
};
}
bool State::replaceBlockWithPreparedBlock(
const ReplaceTarget &target,
Block block) {
return applyCheckedMutation(false, [
target,
block = std::move(block)
](State &candidate) mutable {
const auto &path = target.path;
const auto blocks = candidate.blockContainer(path.container);
if (!blocks
|| path.index < 0
|| path.index >= int(blocks->size())) {
return CheckedMutationResult<bool>{ .result = false };
}
auto &current = (*blocks)[path.index];
if (!BlockMatchesReplaceTarget(current, target)
|| !IsReplaceableMediaBlockKind(block.kind)) {
return CheckedMutationResult<bool>{ .result = false };
}
block.caption = std::move(current.caption);
block.anchorId = std::move(current.anchorId);
if (IsPhotoVideoBlockKind(current.kind)
&& IsPhotoVideoBlockKind(block.kind)) {
block.spoiler = current.spoiler;
}
current = std::move(block);
candidate.rebuild();
return CheckedMutationResult<bool>{
.apply = true,
.result = true,
};
});
}
std::optional<int> State::removeBlock(
const BlockPath &path,
bool forward) {
return removeStructuralSelection(preparedSelectionForBlock(path), forward);
}
bool State::canGroupPhotoVideoBlocks(
const PreparedEditSelection &selection) const {
if (selection.kind != PreparedEditSelectionKind::Blocks) {
return false;
}
const auto range = validateBlockRange(selection.blocks);
if (!range || range->till - range->from < 2) {
return false;
}
const auto blocks = blockContainer(range->container);
if (!blocks) {
return false;
}
for (auto i = range->from; i != range->till; ++i) {
if (!IsPhotoVideoBlockKind((*blocks)[i].kind)) {
return false;
}
}
return HasValidGroupingCaptionAndAnchorSource(
*blocks,
range->from,
range->till);
}
bool State::groupPhotoVideoBlocks(
const PreparedEditSelection &selection,
RichPage::GroupedMediaIntent intent) {
return applyCheckedMutation(false, [selection, intent](State &candidate) {
if (!candidate.canGroupPhotoVideoBlocks(selection)) {
return CheckedMutationResult<bool>{ .result = false };
}
const auto range = candidate.validateBlockRange(selection.blocks);
if (!range) {
return CheckedMutationResult<bool>{ .result = false };
}
auto *blocks = candidate.blockContainer(range->container);
if (!blocks) {
return CheckedMutationResult<bool>{ .result = false };
}
auto items = std::vector<RichPage::GroupedMediaItem>();
items.reserve(range->till - range->from);
auto captionSource = std::optional<int>();
for (auto i = range->from; i != range->till; ++i) {
const auto item = GroupedItemFromPhotoVideoBlock((*blocks)[i]);
if (!item) {
return CheckedMutationResult<bool>{ .result = false };
}
items.push_back(*item);
if (!BlockHasGroupingCaptionOrAnchor((*blocks)[i])) {
continue;
} else if (captionSource) {
return CheckedMutationResult<bool>{ .result = false };
}
captionSource = i;
}
auto grouped = Block();
grouped.kind = BlockKind::GroupedMedia;
grouped.mediaIntent = intent;
grouped.mediaItems = std::move(items);
if (captionSource) {
auto &source = (*blocks)[*captionSource];
grouped.caption = std::move(source.caption);
grouped.anchorId = std::move(source.anchorId);
}
blocks->erase(
blocks->begin() + range->from,
blocks->begin() + range->till);
blocks->insert(blocks->begin() + range->from, std::move(grouped));
candidate.rebuild();
return CheckedMutationResult<bool>{
.apply = true,
.result = true,
};
});
}
bool State::ungroupGroupedMediaBlock(const BlockPath &path) {
return applyCheckedMutation(false, [path](State &candidate) {
auto *blocks = candidate.blockContainer(path.container);
if (!blocks
|| path.index < 0
|| path.index >= int(blocks->size())) {
return CheckedMutationResult<bool>{ .result = false };
}
auto &current = (*blocks)[path.index];
if (current.kind != BlockKind::GroupedMedia
|| current.mediaItems.empty()) {
return CheckedMutationResult<bool>{ .result = false };
}
auto emitted = std::vector<Block>();
emitted.reserve(current.mediaItems.size());
for (const auto &item : current.mediaItems) {
auto block = PhotoVideoBlockFromGroupedItem(item);
if (!block) {
return CheckedMutationResult<bool>{ .result = false };
}
emitted.push_back(std::move(*block));
}
emitted.front().caption = std::move(current.caption);
emitted.front().anchorId = std::move(current.anchorId);
blocks->erase(blocks->begin() + path.index);
blocks->insert(
blocks->begin() + path.index,
std::make_move_iterator(emitted.begin()),
std::make_move_iterator(emitted.end()));
candidate.rebuild();
return CheckedMutationResult<bool>{
.apply = true,
.result = true,
};
});
}
bool State::removeGroupedItem(
const BlockPath &path,
int itemIndex) {
if (itemIndex < 0) {
return false;
}
return applyCheckedMutation(false, [path, itemIndex](State &candidate) {
auto *blocks = candidate.blockContainer(path.container);
if (!blocks
|| path.index < 0
|| path.index >= int(blocks->size())) {
return CheckedMutationResult<bool>{ .result = false };
}
auto &current = (*blocks)[path.index];
if (current.kind != BlockKind::GroupedMedia
|| itemIndex >= int(current.mediaItems.size())) {
return CheckedMutationResult<bool>{ .result = false };
}
current.mediaItems.erase(current.mediaItems.begin() + itemIndex);
if (current.mediaItems.size() >= 2) {
candidate.rebuild();
return CheckedMutationResult<bool>{
.apply = true,
.result = true,
};
}
if (current.mediaItems.empty()) {
blocks->erase(blocks->begin() + path.index);
candidate.rebuild();
return CheckedMutationResult<bool>{
.apply = true,
.result = true,
};
}
auto single = PhotoVideoBlockFromGroupedItem(current.mediaItems.front());
if (!single) {
return CheckedMutationResult<bool>{ .result = false };
}
single->caption = std::move(current.caption);
single->anchorId = std::move(current.anchorId);
(*blocks)[path.index] = std::move(*single);
candidate.rebuild();
return CheckedMutationResult<bool>{
.apply = true,
.result = true,
};
});
}
bool State::addItemsToGroupedMedia(
const BlockPath &path,
int insertedCount) {
if (insertedCount < 1) {
return false;
}
return applyCheckedMutation(false, [path, insertedCount](State &candidate) {
auto *blocks = candidate.blockContainer(path.container);
if (!blocks
|| path.index < 0
|| path.index >= int(blocks->size())) {
return CheckedMutationResult<bool>{ .result = false };
}
const auto from = path.index + 1;
const auto till = from + insertedCount;
if (till > int(blocks->size())) {
return CheckedMutationResult<bool>{ .result = false };
}
auto &group = (*blocks)[path.index];
if (group.kind != BlockKind::GroupedMedia) {
return CheckedMutationResult<bool>{ .result = false };
}
auto appended = std::vector<RichPage::GroupedMediaItem>();
appended.reserve(insertedCount);
for (auto i = from; i != till; ++i) {
const auto item = GroupedItemFromPhotoVideoBlock((*blocks)[i]);
if (!item) {
return CheckedMutationResult<bool>{ .result = false };
}
appended.push_back(*item);
}
group.mediaItems.insert(
group.mediaItems.end(),
std::make_move_iterator(appended.begin()),
std::make_move_iterator(appended.end()));
blocks->erase(blocks->begin() + from, blocks->begin() + till);
candidate.rebuild();
return CheckedMutationResult<bool>{
.apply = true,
.result = true,
};
});
}
bool State::setGroupedMediaIntent(
const BlockPath &path,
RichPage::GroupedMediaIntent intent) {
return applyCheckedMutation(false, [path, intent](State &candidate) {
auto *current = candidate.block(path);
if (!current || current->kind != BlockKind::GroupedMedia) {
return CheckedMutationResult<bool>{ .result = false };
} else if (current->mediaIntent == intent) {
return CheckedMutationResult<bool>{ .result = false };
}
current->mediaIntent = intent;
candidate.rebuild();
return CheckedMutationResult<bool>{
.apply = true,
.result = true,
};
});
}
ApplyResult State::applySplitParagraphText(
const TextNodeDescriptor &descriptor,
std::vector<TextWithEntities> chunks) {
if (chunks.empty()) {
return applyActiveTextUnchecked(TextWithEntities());
}
const auto makeParagraph = [&](TextWithEntities text) {
auto paragraph = MakeParagraphBlock();
paragraph.text.text = std::move(text);
return paragraph;
};
const auto focus = [&](LeafPath leaf) {
rebuild();
if (!activateRebuiltLeaf(leaf)) {
ensureActiveTextOrdinal();
}
};
if (descriptor.leaf.kind == LeafKind::BlockText) {
const auto path = descriptor.leaf.block;
if (auto owner = block(path)) {
if (owner->kind == BlockKind::Paragraph) {
auto container = blockContainer(path.container);
if (!container
|| path.index < 0
|| path.index >= int(container->size())) {
return ApplyResult::Failed;
}
clearTemporaryDownParagraph();
owner->text.text = std::move(chunks.front());
auto blocks = std::vector<Block>();
blocks.reserve(chunks.size() - 1);
for (auto i = 1; i != int(chunks.size()); ++i) {
blocks.push_back(makeParagraph(std::move(chunks[i])));
}
container->insert(
container->begin() + path.index + 1,
std::make_move_iterator(blocks.begin()),
std::make_move_iterator(blocks.end()));
focus(descriptor.leaf);
return ApplyResult::Changed;
} else if (owner->kind == BlockKind::Quote && !owner->pullquote) {
clearTemporaryDownParagraph();
auto firstText = std::move(owner->text);
firstText.text = std::move(chunks.front());
auto blocks = std::vector<Block>();
blocks.reserve(chunks.size());
auto first = MakeParagraphBlock();
first.text = std::move(firstText);
blocks.push_back(std::move(first));
for (auto i = 1; i != int(chunks.size()); ++i) {
blocks.push_back(makeParagraph(std::move(chunks[i])));
}
owner->text = RichText();
owner->blocks.insert(
owner->blocks.begin(),
std::make_move_iterator(blocks.begin()),
std::make_move_iterator(blocks.end()));
focus({
.kind = LeafKind::BlockText,
.block = {
.container = BlockChildrenContainer(path),
.index = 0,
},
});
return ApplyResult::Changed;
}
}
} else if (descriptor.leaf.kind == LeafKind::ListItemText) {
const auto path = descriptor.leaf.block;
if (auto item = listItem(path, descriptor.leaf.listItemIndex)) {
clearTemporaryDownParagraph();
auto firstText = std::move(item->text);
firstText.text = std::move(chunks.front());
auto blocks = std::vector<Block>();
blocks.reserve(chunks.size());
auto first = MakeParagraphBlock();
first.anchorId = std::move(item->anchorId);
first.text = std::move(firstText);
blocks.push_back(std::move(first));
for (auto i = 1; i != int(chunks.size()); ++i) {
blocks.push_back(makeParagraph(std::move(chunks[i])));
}
item->anchorId.clear();
item->text = RichText();
item->blocks.insert(
item->blocks.begin(),
std::make_move_iterator(blocks.begin()),
std::make_move_iterator(blocks.end()));
focus({
.kind = LeafKind::BlockText,
.block = {
.container = ListItemChildrenContainer(
path,
descriptor.leaf.listItemIndex),
.index = 0,
},
});
return ApplyResult::Changed;
}
}
return ApplyResult::Failed;
}
std::optional<RichMessageLimitError> State::lastLimitError() const {
return _lastLimitError;
}
std::optional<QString> State::codeBlockLanguage(int ordinal) const {
const auto descriptor = textNode(ordinal);
if (!descriptor || descriptor->leaf.kind != LeafKind::BlockText) {
return std::nullopt;
}
const auto owner = block(descriptor->leaf.block);
return (owner && owner->kind == BlockKind::Code)
? std::make_optional(owner->language)
: std::nullopt;
}
bool State::setCodeBlockLanguage(int ordinal, QString language) {
const auto descriptor = textNode(ordinal);
if (!descriptor || descriptor->leaf.kind != LeafKind::BlockText) {
return false;
}
auto owner = block(descriptor->leaf.block);
if (!owner || owner->kind != BlockKind::Code) {
return false;
}
owner->language = std::move(language).trimmed();
rebuild();
return true;
}
bool State::toggleTaskState(
const Markdown::PreparedEditListItemSource &source) {
const auto blockPath = convertBlockPath(source.block);
if (!blockPath) {
return false;
}
auto item = listItem(*blockPath, source.listItemIndex);
if (!item || item->taskState == TaskState::None) {
return false;
}
item->taskState = (item->taskState == TaskState::Unchecked)
? TaskState::Checked
: TaskState::Unchecked;
rebuild();
return true;
}
bool State::toggleDetailsOpen(
const Markdown::PreparedEditBlockSource &source) {
const auto path = convertBlockPath(source);
if (!path) {
return false;
}
auto owner = block(*path);
if (!owner || owner->kind != BlockKind::Details) {
return false;
}
owner->open = !owner->open;
rebuild();
return true;
}
State::ListSelectionInfo State::listSelectionInfo(
const PreparedEditListItemRange &range) const {
const auto validated = validateListItemRange(range);
const auto owner = validated ? block(validated->block) : nullptr;
if (!validated || !owner || owner->kind != BlockKind::List) {
return {};
}
auto result = ListSelectionInfo{
.valid = true,
.taskList = IsTaskList(owner->listItems),
.wholeList = (validated->from == 0)
&& (validated->till == int(owner->listItems.size())),
.singleItem = (validated->till == validated->from + 1),
.reversed = (owner->listKind == ListKind::Ordered)
&& owner->orderedList.reversed,
.selectedItems = validated->till - validated->from,
.listKind = owner->listKind,
};
if (owner->listKind != ListKind::Ordered) {
return result;
}
result.allOrderedDecimal = true;
result.allOrderedLowerAlpha = true;
result.allOrderedUpperAlpha = true;
result.allOrderedLowerRoman = true;
result.allOrderedUpperRoman = true;
for (auto i = validated->from; i != validated->till; ++i) {
const auto type = EffectiveOrderedListType(*owner, owner->listItems[i]);
result.allOrderedDecimal = result.allOrderedDecimal
&& (type == PreparedOrderedListType::Decimal);
result.allOrderedLowerAlpha = result.allOrderedLowerAlpha
&& (type == PreparedOrderedListType::LowerAlpha);
result.allOrderedUpperAlpha = result.allOrderedUpperAlpha
&& (type == PreparedOrderedListType::UpperAlpha);
result.allOrderedLowerRoman = result.allOrderedLowerRoman
&& (type == PreparedOrderedListType::LowerRoman);
result.allOrderedUpperRoman = result.allOrderedUpperRoman
&& (type == PreparedOrderedListType::UpperRoman);
}
return result;
}
std::optional<PreparedEditListItemRange> State::listContextRangeForSelection(
const PreparedEditSelection &selection,
const PreparedEditListItemSource &source) const {
if (source.listItemIndex < 0) {
return std::nullopt;
}
const auto sourceBlock = convertBlockPath(source.block);
const auto owner = sourceBlock ? block(*sourceBlock) : nullptr;
if (!sourceBlock
|| !owner
|| owner->kind != BlockKind::List
|| source.listItemIndex >= int(owner->listItems.size())) {
return std::nullopt;
}
switch (selection.kind) {
case PreparedEditSelectionKind::ListItems: {
const auto range = validateListItemRange(selection.listItems);
if (!range
|| range->block != *sourceBlock
|| source.listItemIndex < range->from
|| source.listItemIndex >= range->till) {
return std::nullopt;
}
return selection.listItems;
}
case PreparedEditSelectionKind::Blocks: {
const auto range = validateBlockRange(selection.blocks);
if (!range
|| sourceBlock->container != range->container
|| sourceBlock->index < range->from
|| sourceBlock->index >= range->till) {
return std::nullopt;
}
return PreparedEditListItemRange{
.block = source.block,
.from = 0,
.till = int(owner->listItems.size()),
};
}
case PreparedEditSelectionKind::TableRows:
case PreparedEditSelectionKind::TableCells:
case PreparedEditSelectionKind::None:
return std::nullopt;
}
return std::nullopt;
}
bool State::setListStyle(
const PreparedEditListItemRange &range,
ListStyle style) {
const auto validated = validateListItemRange(range);
auto owner = validated ? block(validated->block) : nullptr;
if (!validated || !owner || owner->kind != BlockKind::List) {
return false;
}
auto changed = false;
const auto current = CurrentListStyle(*owner);
if (current == style) {
if (style == ListStyle::Ordered) {
changed = ClearOrderedTaskStates(owner);
}
if (changed) {
rebuild();
}
return true;
}
switch (style) {
case ListStyle::Ordered:
owner->listKind = ListKind::Ordered;
owner->orderedList = {};
changed = true;
for (auto &item : owner->listItems) {
if (item.taskState != TaskState::None) {
item.taskState = TaskState::None;
}
item.number = {};
}
break;
case ListStyle::Bullet:
owner->listKind = ListKind::Bullet;
changed = ResetNonOrderedListMetadata(owner) || changed;
for (auto &item : owner->listItems) {
if (item.taskState != TaskState::None) {
item.taskState = TaskState::None;
changed = true;
}
}
break;
case ListStyle::Task:
owner->listKind = ListKind::Bullet;
changed = ResetNonOrderedListMetadata(owner) || changed;
for (auto &item : owner->listItems) {
if (item.taskState == TaskState::None) {
item.taskState = TaskState::Unchecked;
changed = true;
}
}
break;
}
if (changed) {
rebuild();
}
return true;
}
bool State::setListOrderedType(
const PreparedEditListItemRange &range,
PreparedOrderedListType type) {
const auto validated = validateListItemRange(range);
auto owner = validated ? block(validated->block) : nullptr;
if (!validated
|| !owner
|| owner->kind != BlockKind::List
|| owner->listKind != ListKind::Ordered) {
return false;
}
auto changed = false;
const auto stored = StoredOrderedListType(type);
if (owner->orderedList.type != stored) {
owner->orderedList.type = stored;
for (auto &item : owner->listItems) {
if (!item.number.type.has_value()
&& item.number.num.has_value()) {
item.number.num = std::nullopt;
}
}
changed = true;
}
if (changed) {
rebuild();
}
return true;
}
bool State::setListOrderedReversed(
const PreparedEditListItemRange &range,
bool reversed) {
const auto validated = validateListItemRange(range);
auto owner = validated ? block(validated->block) : nullptr;
if (!validated
|| !owner
|| owner->kind != BlockKind::List
|| owner->listKind != ListKind::Ordered) {
return false;
}
auto changed = false;
if (owner->orderedList.reversed != reversed) {
owner->orderedList.reversed = reversed;
(void)ClearOrderedListRawMarkers(owner);
changed = true;
}
if (changed) {
rebuild();
}
return true;
}
bool State::setListItemOrderedType(
const PreparedEditListItemRange &range,
std::optional<PreparedOrderedListType> type) {
const auto validated = validateListItemRange(range);
auto owner = validated ? block(validated->block) : nullptr;
if (!validated
|| !owner
|| owner->kind != BlockKind::List
|| owner->listKind != ListKind::Ordered) {
return false;
}
auto changed = false;
const auto parentType = ResolvePreparedOrderedListType(owner->orderedList.type);
const auto stored = (type && (*type != parentType))
? StoredOrderedListType(*type, (*type == PreparedOrderedListType::Decimal))
: std::optional<QString>();
for (auto i = validated->from; i != validated->till; ++i) {
auto &item = owner->listItems[i];
if (item.number.type != stored) {
item.number.type = stored;
if (item.number.num.has_value()) {
item.number.num = std::nullopt;
}
changed = true;
}
}
if (changed) {
rebuild();
}
return true;
}
State::TableSelectionInfo State::tableSelectionInfo(
const Markdown::PreparedEditTableCellRange &range) const {
const auto validated = validateTableCellRange(range);
if (!validated) {
return {};
}
const auto owner = block(validated->block);
if (!owner || owner->kind != BlockKind::Table) {
return {};
}
const auto grid = BuildTableGrid(*owner, tableRenderLimits());
const auto selected = SelectedTableGridCells(grid, *validated);
if (selected.empty()) {
return {};
}
auto result = TableSelectionInfo{
.valid = true,
.allHeader = true,
.allAlignLeft = true,
.allAlignCenter = true,
.allAlignRight = true,
.allAlignTop = true,
.allAlignMiddle = true,
.allAlignBottom = true,
.singleCell = (selected.size() == 1),
.canDeleteRows = TableGridRangeSpansAllColumns(grid, *validated),
.canDeleteColumns = TableGridRangeSpansAllRows(grid, *validated),
.canDeleteTable = TableGridRangeCoversFullTable(grid, *validated),
.selectedRows = validated->rowTill - validated->rowFrom,
.selectedColumns = validated->columnTill - validated->columnFrom,
.bordered = owner->bordered,
.striped = owner->striped,
};
for (const auto &reference : selected) {
const auto &cell = owner->tableRows[reference.rowIndex].cells[
reference.cellIndex];
if (!cell.header) {
result.allHeader = false;
}
if (cell.alignment != RichPage::TableAlignment::Left) {
result.allAlignLeft = false;
}
if (cell.alignment != RichPage::TableAlignment::Center) {
result.allAlignCenter = false;
}
if (cell.alignment != RichPage::TableAlignment::Right) {
result.allAlignRight = false;
}
if (cell.verticalAlignment != RichPage::TableVerticalAlignment::Top) {
result.allAlignTop = false;
}
if (cell.verticalAlignment
!= RichPage::TableVerticalAlignment::Middle) {
result.allAlignMiddle = false;
}
if (cell.verticalAlignment
!= RichPage::TableVerticalAlignment::Bottom) {
result.allAlignBottom = false;
}
if (result.singleCell) {
result.canSplitCell = (NormalizeTableSpan(cell.colspan) > 1)
|| (NormalizeTableSpan(cell.rowspan) > 1);
}
}
result.canUniteCells = !result.singleCell
&& CleanTableGridUniteRange(grid, *validated);
return result;
}
std::optional<Markdown::PreparedEditTableCellRange>
State::tableContextRangeForSelection(
const Markdown::PreparedEditSelection &selection,
const Markdown::PreparedEditTableCellSource &source) const {
if (source.tableRowIndex < 0
|| source.column < 0
|| source.rowspan <= 0
|| source.colspan <= 0) {
return std::nullopt;
}
const auto sourceBlock = convertBlockPath(source.block);
if (!sourceBlock) {
return std::nullopt;
}
const auto owner = block(*sourceBlock);
if (!owner || owner->kind != BlockKind::Table) {
return std::nullopt;
}
const auto grid = BuildTableGrid(*owner, tableRenderLimits());
if (grid.rowCount <= 0 || grid.columnCount <= 0) {
return std::nullopt;
}
const auto fullTableRange = [&] {
return Markdown::PreparedEditTableCellRange{
.block = source.block,
.rowFrom = 0,
.rowTill = grid.rowCount,
.columnFrom = 0,
.columnTill = grid.columnCount,
};
};
const auto sourceIntersects = [&](const auto &range) {
return (source.tableRowIndex < range.rowTill)
&& (source.tableRowIndex + source.rowspan > range.rowFrom)
&& (source.column < range.columnTill)
&& (source.column + source.colspan > range.columnFrom);
};
switch (selection.kind) {
case PreparedEditSelectionKind::TableCells: {
const auto range = validateTableCellRange(selection.tableCells);
if (!range || range->block != *sourceBlock) {
return std::nullopt;
}
return sourceIntersects(*range)
? std::make_optional(selection.tableCells)
: std::nullopt;
}
case PreparedEditSelectionKind::TableRows: {
const auto range = validateTableRowRange(selection.tableRows);
if (!range || range->block != *sourceBlock) {
return std::nullopt;
}
if (source.tableRowIndex >= range->till
|| source.tableRowIndex + source.rowspan <= range->from) {
return std::nullopt;
}
return Markdown::PreparedEditTableCellRange{
.block = source.block,
.rowFrom = range->from,
.rowTill = range->till,
.columnFrom = 0,
.columnTill = grid.columnCount,
};
}
case PreparedEditSelectionKind::Blocks: {
const auto range = validateBlockRange(selection.blocks);
if (!range
|| sourceBlock->container != range->container
|| sourceBlock->index < range->from
|| sourceBlock->index >= range->till) {
return std::nullopt;
}
return fullTableRange();
}
case PreparedEditSelectionKind::ListItems:
case PreparedEditSelectionKind::None:
return std::nullopt;
}
return std::nullopt;
}
bool State::canRemoveStructuralSelection(
const Markdown::PreparedEditSelection &selection) const {
switch (selection.kind) {
case PreparedEditSelectionKind::Blocks:
return validateBlockRange(selection.blocks).has_value();
case PreparedEditSelectionKind::ListItems:
return validateListItemRange(selection.listItems).has_value();
case PreparedEditSelectionKind::TableRows:
return validateTableRowRange(selection.tableRows).has_value();
case PreparedEditSelectionKind::TableCells: {
const auto range = validateTableCellRange(selection.tableCells);
if (!range) {
return false;
}
const auto owner = block(range->block);
if (!owner || owner->kind != BlockKind::Table) {
return false;
}
return TableGridRangeSpansAllRows(
BuildTableGrid(*owner, tableRenderLimits()),
*range);
}
case PreparedEditSelectionKind::None:
return false;
}
return false;
}
auto State::structuredClipboardDataForSelection(
const Markdown::PreparedEditSelection &selection) const
-> std::optional<ClipboardData> {
switch (selection.kind) {
case PreparedEditSelectionKind::Blocks: {
const auto range = validateBlockRange(selection.blocks);
const auto blocks = range ? blockContainer(range->container) : nullptr;
if (!range || !blocks) {
return std::nullopt;
}
auto data = ClipboardBlockData();
data.blocks = std::vector<Block>(
blocks->begin() + range->from,
blocks->begin() + range->till);
return ClipboardData(std::move(data));
}
case PreparedEditSelectionKind::ListItems: {
const auto range = validateListItemRange(selection.listItems);
const auto owner = range ? block(range->block) : nullptr;
if (!range || !owner || owner->kind != BlockKind::List) {
return std::nullopt;
}
auto data = ClipboardListItemsData();
data.listKind = owner->listKind;
data.orderedList = owner->orderedList;
data.items = std::vector<ListItem>(
owner->listItems.begin() + range->from,
owner->listItems.begin() + range->till);
data.taskList = IsTaskList(data.items);
return ClipboardData(std::move(data));
}
case PreparedEditSelectionKind::TableRows:
case PreparedEditSelectionKind::TableCells:
case PreparedEditSelectionKind::None:
return std::nullopt;
}
return std::nullopt;
}
std::shared_ptr<const RichPage> State::richPageForTableSelection(
const Markdown::PreparedEditSelection &selection) const {
auto blockPath = BlockPath();
auto rowFrom = 0;
auto rowTill = 0;
auto columnFrom = -1;
auto columnTill = -1;
if (selection.kind == PreparedEditSelectionKind::TableCells) {
const auto range = validateTableCellRange(selection.tableCells);
if (!range) {
return nullptr;
}
blockPath = range->block;
rowFrom = range->rowFrom;
rowTill = range->rowTill;
columnFrom = range->columnFrom;
columnTill = range->columnTill;
} else if (selection.kind == PreparedEditSelectionKind::TableRows) {
const auto range = validateTableRowRange(selection.tableRows);
if (!range) {
return nullptr;
}
blockPath = range->block;
rowFrom = range->from;
rowTill = range->till;
} else {
return nullptr;
}
const auto owner = block(blockPath);
if (!owner || owner->kind != BlockKind::Table) {
return nullptr;
}
const auto grid = BuildTableGrid(*owner, tableRenderLimits());
if (columnFrom < 0 || columnTill < 0) {
columnFrom = 0;
columnTill = grid.columnCount;
}
if (rowFrom < 0
|| rowTill <= rowFrom
|| columnFrom < 0
|| columnTill <= columnFrom) {
return nullptr;
}
const auto rectangle = StructuralTableCellRange{
.rowFrom = rowFrom,
.rowTill = rowTill,
.columnFrom = columnFrom,
.columnTill = columnTill,
};
const auto references = SelectedTableGridCells(grid, rectangle);
if (references.empty()) {
return nullptr;
}
auto page = std::make_shared<RichPage>();
if (references.size() == 1
&& (rowTill - rowFrom == 1)
&& (columnTill - columnFrom == 1)) {
const auto &reference = references.front();
const auto &row = owner->tableRows[reference.rowIndex];
auto paragraph = MakeParagraphBlock();
paragraph.text = row.cells[reference.cellIndex].text;
page->blocks.push_back(std::move(paragraph));
return page;
}
auto table = Block();
table.kind = BlockKind::Table;
table.bordered = owner->bordered;
table.striped = owner->striped;
table.tableRows.resize(rowTill - rowFrom);
for (const auto &reference : references) {
auto cell = owner->tableRows[reference.rowIndex]
.cells[reference.cellIndex];
const auto clampedRowFrom = std::max(reference.rowFrom, rowFrom);
const auto clampedRowTill = std::min(reference.rowTill, rowTill);
const auto clampedColumnFrom = std::max(reference.columnFrom, columnFrom);
const auto clampedColumnTill = std::min(reference.columnTill, columnTill);
cell.rowspan = std::max(clampedRowTill - clampedRowFrom, 1);
cell.colspan = std::max(clampedColumnTill - clampedColumnFrom, 1);
table.tableRows[clampedRowFrom - rowFrom].cells.push_back(
std::move(cell));
}
page->blocks.push_back(std::move(table));
return page;
}
bool State::insertPreparedBlocksAfterTableSelection(
const Markdown::PreparedEditSelection &selection,
std::vector<RichPage::Block> blocks) {
if (blocks.empty()) {
return false;
}
auto blockPath = std::optional<Markdown::PreparedEditBlockPath>();
if (selection.kind == PreparedEditSelectionKind::TableCells) {
if (selection.tableCells.empty()) {
return false;
}
blockPath = selection.tableCells.block;
} else if (selection.kind == PreparedEditSelectionKind::TableRows) {
if (selection.tableRows.empty()) {
return false;
}
blockPath = selection.tableRows.block;
} else {
return false;
}
const auto path = *blockPath;
return applyCheckedMutation(false, [
blocks = std::move(blocks),
path](State &candidate) mutable {
const auto container = candidate.convertBlockContainerPath(
path.container);
if (!container || !candidate.blockContainer(*container)) {
return CheckedMutationResult<bool>{ .result = false };
}
candidate.normalizeInsertedBlockAnchors(blocks);
auto insertAt = path.index + 1;
if (!candidate.insertPreparedBlocksAtExplicitPosition(
std::move(blocks),
*container,
&insertAt)) {
return CheckedMutationResult<bool>{ .result = false };
}
candidate.rebuild();
return CheckedMutationResult<bool>{ .apply = true, .result = true };
});
}
bool State::addTableRow(
const Markdown::PreparedEditTableCellRange &range,
bool after) {
return applyCheckedMutation(false, [range, after](State &candidate) {
const auto applied = candidate.addTableRowUnchecked(range, after);
return CheckedMutationResult<bool>{
.apply = applied,
.result = applied,
};
});
}
bool State::addTableRowUnchecked(
const Markdown::PreparedEditTableCellRange &range,
bool after) {
const auto validated = validateTableCellRange(range);
if (!validated) {
return false;
}
auto owner = block(validated->block);
if (!owner || owner->kind != BlockKind::Table) {
return false;
}
const auto insertAt = after ? validated->rowTill : validated->rowFrom;
const auto grid = BuildTableGrid(*owner, tableRenderLimits());
if (insertAt < 0 || insertAt > int(owner->tableRows.size())) {
return false;
}
const auto sourceRow = after
? validated->rowTill - 1
: validated->rowFrom;
auto coveredColumns = std::vector<char>(grid.columnCount, false);
for (const auto &reference : grid.cells) {
if (reference.rowFrom >= insertAt || reference.rowTill <= insertAt) {
continue;
}
auto &cell = owner->tableRows[reference.rowIndex].cells[
reference.cellIndex];
cell.rowspan = IncrementTableSpan(cell.rowspan);
for (auto column = reference.columnFrom;
column != reference.columnTill;
++column) {
coveredColumns[column] = true;
}
}
auto row = TableRow();
if (grid.columnCount <= 0) {
row.cells.push_back(MakeDefaultTableCell());
} else {
row.cells.reserve(grid.columnCount);
for (auto column = 0; column != grid.columnCount; ++column) {
if (!coveredColumns[column]) {
const auto source = TableGridCellAt(
*owner,
grid,
sourceRow,
column);
row.cells.push_back(MakeDefaultTableCell(
source ? source->header : false));
}
}
}
owner->tableRows.insert(
owner->tableRows.begin() + insertAt,
std::move(row));
rebuild();
return true;
}
bool State::addTableColumn(
const Markdown::PreparedEditTableCellRange &range,
bool after) {
return applyCheckedMutation(false, [range, after](State &candidate) {
const auto applied = candidate.addTableColumnUnchecked(range, after);
return CheckedMutationResult<bool>{
.apply = applied,
.result = applied,
};
});
}
bool State::addTableColumnUnchecked(
const Markdown::PreparedEditTableCellRange &range,
bool after) {
const auto validated = validateTableCellRange(range);
if (!validated) {
return false;
}
auto owner = block(validated->block);
if (!owner || owner->kind != BlockKind::Table) {
return false;
}
const auto insertAt = after
? validated->columnTill
: validated->columnFrom;
const auto grid = BuildTableGrid(*owner, tableRenderLimits());
const auto sourceColumn = after
? validated->columnTill - 1
: validated->columnFrom;
auto coveredRows = std::vector<char>(owner->tableRows.size(), false);
for (const auto &reference : grid.cells) {
if (reference.columnFrom >= insertAt
|| reference.columnTill <= insertAt) {
continue;
}
auto &cell = owner->tableRows[reference.rowIndex].cells[
reference.cellIndex];
cell.colspan = IncrementTableSpan(cell.colspan);
for (auto row = reference.rowFrom; row != reference.rowTill; ++row) {
coveredRows[row] = true;
}
}
for (auto rowIndex = 0;
rowIndex != int(owner->tableRows.size());
++rowIndex) {
if (coveredRows[rowIndex]) {
continue;
}
const auto source = TableGridCellAt(
*owner,
grid,
rowIndex,
sourceColumn);
InsertTableCellBeforeVisualColumn(
&owner->tableRows[rowIndex],
grid,
rowIndex,
insertAt,
MakeDefaultTableCell(source ? source->header : false));
}
rebuild();
return true;
}
bool State::setTableHeader(
const Markdown::PreparedEditTableCellRange &range,
bool header) {
const auto validated = validateTableCellRange(range);
if (!validated) {
return false;
}
auto owner = block(validated->block);
if (!owner || owner->kind != BlockKind::Table) {
return false;
}
const auto selected = SelectedTableGridCells(
BuildTableGrid(*owner, tableRenderLimits()),
*validated);
if (selected.empty()) {
return false;
}
auto changed = false;
for (const auto &reference : selected) {
auto &cell = owner->tableRows[reference.rowIndex].cells[
reference.cellIndex];
if (cell.header != header) {
cell.header = header;
changed = true;
}
}
if (changed) {
rebuild();
}
return true;
}
bool State::setTableAlignment(
const Markdown::PreparedEditTableCellRange &range,
RichPage::TableAlignment alignment) {
const auto validated = validateTableCellRange(range);
if (!validated) {
return false;
}
auto owner = block(validated->block);
if (!owner || owner->kind != BlockKind::Table) {
return false;
}
const auto selected = SelectedTableGridCells(
BuildTableGrid(*owner, tableRenderLimits()),
*validated);
if (selected.empty()) {
return false;
}
auto changed = false;
for (const auto &reference : selected) {
auto &cell = owner->tableRows[reference.rowIndex].cells[
reference.cellIndex];
if (cell.alignment != alignment) {
cell.alignment = alignment;
changed = true;
}
}
if (changed) {
rebuild();
}
return true;
}
bool State::setTableVerticalAlignment(
const Markdown::PreparedEditTableCellRange &range,
RichPage::TableVerticalAlignment alignment) {
const auto validated = validateTableCellRange(range);
if (!validated) {
return false;
}
auto owner = block(validated->block);
if (!owner || owner->kind != BlockKind::Table) {
return false;
}
const auto selected = SelectedTableGridCells(
BuildTableGrid(*owner, tableRenderLimits()),
*validated);
if (selected.empty()) {
return false;
}
auto changed = false;
for (const auto &reference : selected) {
auto &cell = owner->tableRows[reference.rowIndex].cells[
reference.cellIndex];
if (cell.verticalAlignment != alignment) {
cell.verticalAlignment = alignment;
changed = true;
}
}
if (changed) {
rebuild();
}
return true;
}
bool State::splitTableCell(
const Markdown::PreparedEditTableCellRange &range) {
const auto validated = validateTableCellRange(range);
if (!validated) {
return false;
}
auto owner = block(validated->block);
if (!owner || owner->kind != BlockKind::Table) {
return false;
}
const auto grid = BuildTableGrid(*owner, tableRenderLimits());
const auto selected = SelectedTableGridCells(grid, *validated);
if (selected.size() != 1) {
return false;
}
const auto reference = selected.front();
auto &cell = owner->tableRows[reference.rowIndex].cells[
reference.cellIndex];
if (cell.rowspan <= 1 && cell.colspan <= 1) {
return false;
}
const auto header = cell.header;
cell.rowspan = 1;
cell.colspan = 1;
for (auto rowIndex = reference.rowFrom;
rowIndex != reference.rowTill;
++rowIndex) {
auto &row = owner->tableRows[rowIndex];
for (auto column = reference.columnTill;
column != reference.columnFrom;
--column) {
const auto currentColumn = column - 1;
if (rowIndex == reference.rowFrom
&& currentColumn == reference.columnFrom) {
continue;
}
InsertTableCellBeforeVisualColumn(
&row,
grid,
rowIndex,
currentColumn,
MakeDefaultTableCell(header));
}
}
rebuild();
return true;
}
bool State::uniteTableCells(
const Markdown::PreparedEditTableCellRange &range) {
const auto validated = validateTableCellRange(range);
if (!validated) {
return false;
}
auto owner = block(validated->block);
if (!owner || owner->kind != BlockKind::Table) {
return false;
}
const auto grid = BuildTableGrid(*owner, tableRenderLimits());
const auto selected = SelectedTableGridCells(grid, *validated);
if (selected.size() <= 1
|| !CleanTableGridUniteRange(grid, *validated)) {
return false;
}
const auto keeper = *std::min_element(
selected.begin(),
selected.end(),
[](const auto &a, const auto &b) {
if (a.rowFrom != b.rowFrom) {
return a.rowFrom < b.rowFrom;
} else if (a.columnFrom != b.columnFrom) {
return a.columnFrom < b.columnFrom;
} else if (a.rowIndex != b.rowIndex) {
return a.rowIndex < b.rowIndex;
}
return a.cellIndex < b.cellIndex;
});
auto &keeperCell = owner->tableRows[keeper.rowIndex].cells[
keeper.cellIndex];
keeperCell.rowspan = validated->rowTill - validated->rowFrom;
keeperCell.colspan = validated->columnTill - validated->columnFrom;
auto toErase = selected;
toErase.erase(
std::remove_if(
toErase.begin(),
toErase.end(),
[&](const TableGridCellReference &cell) {
return cell.rowIndex == keeper.rowIndex
&& cell.cellIndex == keeper.cellIndex;
}),
toErase.end());
std::sort(
toErase.begin(),
toErase.end(),
[](const auto &a, const auto &b) {
if (a.rowIndex != b.rowIndex) {
return a.rowIndex > b.rowIndex;
}
return a.cellIndex > b.cellIndex;
});
for (const auto &reference : toErase) {
auto &row = owner->tableRows[reference.rowIndex];
row.cells.erase(row.cells.begin() + reference.cellIndex);
}
rebuild();
return true;
}
bool State::removeTableRows(
const Markdown::PreparedEditTableCellRange &range) {
const auto validated = validateTableCellRange(range);
if (!validated) {
return false;
}
const auto owner = block(validated->block);
if (!owner || owner->kind != BlockKind::Table) {
return false;
}
if (!TableGridRangeSpansAllColumns(
BuildTableGrid(*owner, tableRenderLimits()),
*validated)) {
return false;
}
return removeStructuralSelection({
.kind = PreparedEditSelectionKind::TableRows,
.tableRows = {
.block = range.block,
.from = validated->rowFrom,
.till = validated->rowTill,
},
}, true).has_value();
}
bool State::removeTableColumns(
const Markdown::PreparedEditTableCellRange &range) {
const auto validated = validateTableCellRange(range);
if (!validated) {
return false;
}
const auto owner = block(validated->block);
if (!owner || owner->kind != BlockKind::Table) {
return false;
}
if (!TableGridRangeSpansAllRows(
BuildTableGrid(*owner, tableRenderLimits()),
*validated)) {
return false;
}
return removeStructuralSelection({
.kind = PreparedEditSelectionKind::TableCells,
.tableCells = range,
}, true).has_value();
}
bool State::removeTable(
const Markdown::PreparedEditTableCellRange &range) {
const auto validated = validateTableCellRange(range);
if (!validated) {
return false;
}
const auto owner = block(validated->block);
if (!owner || owner->kind != BlockKind::Table) {
return false;
}
if (!TableGridRangeCoversFullTable(
BuildTableGrid(*owner, tableRenderLimits()),
*validated)) {
return false;
}
return removeStructuralSelection({
.kind = PreparedEditSelectionKind::Blocks,
.blocks = {
.container = range.block.container,
.from = range.block.index,
.till = range.block.index + 1,
},
}, true).has_value();
}
bool State::setTableBordered(
const Markdown::PreparedEditTableCellRange &range,
bool bordered) {
const auto validated = validateTableCellRange(range);
if (!validated) {
return false;
}
auto owner = block(validated->block);
if (!owner || owner->kind != BlockKind::Table) {
return false;
}
if (owner->bordered != bordered) {
owner->bordered = bordered;
rebuild();
}
return true;
}
bool State::setTableStriped(
const Markdown::PreparedEditTableCellRange &range,
bool striped) {
const auto validated = validateTableCellRange(range);
if (!validated) {
return false;
}
auto owner = block(validated->block);
if (!owner || owner->kind != BlockKind::Table) {
return false;
}
if (owner->striped != striped) {
owner->striped = striped;
rebuild();
}
return true;
}
int State::activeTextLength() const {
return activeRawText().size();
}
std::optional<int> State::previousEditableOrdinal() const {
return adjacentEditableOrdinal(false);
}
std::optional<int> State::nextEditableOrdinal() const {
return adjacentEditableOrdinal(true);
}
std::vector<State::BoundaryTarget> State::boundarySteps(bool forward) const {
auto steps = std::vector<BoundaryTarget>();
collectBoundarySteps(
_richPage->blocks,
BlockContainerPath(),
forward,
&steps);
return steps;
}
State::BoundaryTarget State::activeBoundaryTarget(bool forward) const {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor) {
return {};
}
return boundaryTargetForLeaf(
descriptor->leaf,
descriptor,
forward,
true);
}
State::BoundaryTarget State::boundaryTargetForLeaf(
const LeafPath &leaf,
const TextNodeDescriptor *descriptor,
bool forward,
bool allowRemoveDirectly) const {
const auto ordinal = textNodeOrdinal(leaf);
if (ordinal < 0) {
return {};
}
if (!forward) {
const auto owner = block(leaf.block);
if (owner && owner->kind == BlockKind::Table) {
if (leaf.kind == LeafKind::BlockText && CanEditBlock(*owner)) {
return {
.action = BoundaryAction::StructuralSelection,
.structuralSelection = preparedSelectionForBlock(leaf.block),
};
} else if (leaf.kind == LeafKind::TableCellText
&& leaf.tableRowIndex == 0
&& leaf.tableCellIndex == 0) {
const auto titleOrdinal = textNodeOrdinal(LeafPath{
.kind = LeafKind::BlockText,
.block = leaf.block,
});
if (titleOrdinal >= 0) {
return {
.action = BoundaryAction::Text,
.textOrdinal = titleOrdinal,
};
}
}
}
}
const auto prioritizeStructuralStep = [&](const BoundaryTarget &target) {
if (target.action != BoundaryAction::StructuralSelection) {
return false;
}
switch (target.structuralSelection.kind) {
case PreparedEditSelectionKind::Blocks:
if (const auto range = validateBlockRange(
target.structuralSelection.blocks)) {
return leafWillBeRemoved(leaf, *range);
}
break;
case PreparedEditSelectionKind::ListItems:
if (const auto range = validateListItemRange(
target.structuralSelection.listItems)) {
return leafWillBeRemoved(leaf, *range);
}
break;
default:
break;
}
return false;
};
const auto removeDirectly = allowRemoveDirectly
&& descriptor
&& removalTargetIsEmpty(descriptor->removalTarget)
&& shouldRemoveActiveOwnerDirectly(*descriptor);
auto steps = std::vector<BoundaryTarget>();
collectBoundarySteps(
_richPage->blocks,
BlockContainerPath(),
forward,
&steps);
for (auto i = 0, count = int(steps.size()); i != count; ++i) {
const auto &step = steps[i];
if (step.action == BoundaryAction::Text
&& step.textOrdinal == ordinal) {
const auto next = (i + 1 < count)
? steps[i + 1]
: BoundaryTarget();
if (prioritizeStructuralStep(next)) {
return next;
}
if (removeDirectly) {
return {
.action = BoundaryAction::RemoveActiveOwner,
};
}
return next;
}
}
return removeDirectly
? BoundaryTarget{
.action = BoundaryAction::RemoveActiveOwner,
}
: BoundaryTarget();
}
bool State::isActiveTopLevelParagraph() const {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor || !descriptor->leaf.block.container.steps.empty()) {
return false;
}
const auto owner = block(descriptor->leaf.block);
return owner && owner->kind == BlockKind::Paragraph;
}
bool State::isActiveTopLevelParagraphOrHeading() const {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor || !descriptor->leaf.block.container.steps.empty()) {
return false;
}
const auto owner = block(descriptor->leaf.block);
return owner
&& ((owner->kind == BlockKind::Paragraph)
|| (owner->kind == BlockKind::Heading));
}
bool State::activeSurfaceAllowsSeparateLineFormula() const {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor || descriptor->leaf.kind == LeafKind::MathFormula) {
return false;
}
if (isActiveTopLevelParagraph() || activeListItemSurface().has_value()) {
return true;
}
if (descriptor->leaf.kind != LeafKind::BlockText) {
return false;
}
const auto owner = block(descriptor->leaf.block);
if (!owner) {
return false;
}
if (owner->kind == BlockKind::Quote) {
return !owner->pullquote;
}
if (owner->kind != BlockKind::Paragraph) {
return false;
}
const auto &container = descriptor->leaf.block.container;
if (container.steps.empty()) {
return false;
}
const auto &step = container.steps.back();
if (step.kind != BlockContainerKind::BlockChildren) {
return false;
}
auto parent = container;
parent.steps.pop_back();
const auto quote = block({
.container = parent,
.index = step.blockIndex,
});
return quote
&& (quote->kind == BlockKind::Quote)
&& !quote->pullquote;
}
bool State::activeLeafUsesQuoteCaptionColor() const {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor || descriptor->leaf.kind != LeafKind::BlockCaption) {
return false;
}
const auto owner = block(descriptor->leaf.block);
return owner && owner->kind == BlockKind::Quote;
}
bool State::activeLeafUsesQuotePlaceholderColor() const {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor) {
return false;
}
const auto &leaf = descriptor->leaf;
const auto owner = block(leaf.block);
if (owner
&& owner->kind == BlockKind::Quote
&& (leaf.kind == LeafKind::BlockText
|| leaf.kind == LeafKind::BlockCaption)) {
return true;
}
auto container = leaf.block.container;
while (!container.steps.empty()) {
const auto step = container.steps.back();
container.steps.pop_back();
if (step.kind != BlockContainerKind::BlockChildren) {
continue;
}
const auto ancestor = block({
.container = container,
.index = step.blockIndex,
});
if (ancestor
&& ancestor->kind == BlockKind::Quote) {
return true;
}
}
return false;
}
bool State::activeBlockBodyCanEscape() const {
return activeBlockBodyEscapeBlock().has_value();
}
bool State::shouldRemoveActiveOwnerDirectly(
const TextNodeDescriptor &descriptor) const {
switch (descriptor.removalTarget.kind) {
case RemovalKind::Block: {
const auto owner = block(descriptor.removalTarget.block);
if (!owner) {
return false;
}
switch (owner->kind) {
case BlockKind::Heading:
case BlockKind::Paragraph:
case BlockKind::Footer:
case BlockKind::Code:
case BlockKind::Math:
return true;
default:
return false;
}
}
case RemovalKind::ListItem:
if (const auto owner = listItem(
descriptor.removalTarget.block,
descriptor.removalTarget.listItemIndex)) {
return owner->blocks.empty();
}
return false;
case RemovalKind::TableCell:
return false;
}
return false;
}
std::optional<int> State::removeActiveOwnerAndSelectAdjacent(bool forward) {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor || !removalTargetIsEmpty(descriptor->removalTarget)) {
return std::nullopt;
}
const auto target = descriptor->removalTarget;
auto first = _activeTextOrdinal;
auto last = _activeTextOrdinal;
while (first > 0 && _textNodes[first - 1].removalTarget == target) {
--first;
}
while (last + 1 < textNodeCount()
&& _textNodes[last + 1].removalTarget == target) {
++last;
}
auto adjacent = std::optional<LeafPath>();
if (forward) {
for (auto i = last + 1, count = textNodeCount(); i != count; ++i) {
if (!(_textNodes[i].removalTarget == target)) {
adjacent = _textNodes[i].leaf;
break;
}
}
} else {
for (auto i = first; i != 0; --i) {
if (!(_textNodes[i - 1].removalTarget == target)) {
adjacent = _textNodes[i - 1].leaf;
break;
}
}
}
if (!removeTarget(target)) {
return std::nullopt;
}
rebuild();
if (adjacent && (!forward || target.kind == RemovalKind::TableCell)) {
const auto ordinal = textNodeOrdinal(*adjacent);
if (setActiveTextByOrdinal(ordinal)) {
return _activeTextOrdinal;
}
}
if (!textNodeCount()) {
return std::nullopt;
}
const auto fallback = forward
? std::min(first, textNodeCount() - 1)
: std::max(std::min(first - 1, textNodeCount() - 1), 0);
if (!setActiveTextByOrdinal(fallback)) {
ensureActiveTextOrdinal();
}
return _activeTextOrdinal;
}
const TextNodeDescriptor *State::adjacentTextNode(
int ordinal,
bool forward) const {
return textNode(ordinal + (forward ? 1 : -1));
}
bool State::joinActiveParagraphBoundaryUnchecked(
bool forward,
ActiveTextSelectionTarget *target) {
if (!target) {
return false;
}
const auto descriptor = textNode(_activeTextOrdinal);
const auto adjacent = descriptor
? adjacentTextNode(_activeTextOrdinal, forward)
: nullptr;
if (!descriptor
|| !adjacent
|| descriptor->leaf.kind != LeafKind::BlockText
|| adjacent->leaf.kind != LeafKind::BlockText
|| !(descriptor->leaf.block.container == adjacent->leaf.block.container)) {
return false;
}
const auto activeIndex = descriptor->leaf.block.index;
const auto adjacentIndex = adjacent->leaf.block.index;
if (adjacentIndex != activeIndex + (forward ? 1 : -1)) {
return false;
}
auto *blocks = blockContainer(descriptor->leaf.block.container);
if (!blocks
|| activeIndex < 0
|| adjacentIndex < 0
|| activeIndex >= int(blocks->size())
|| adjacentIndex >= int(blocks->size())) {
return false;
}
auto &activeOwner = (*blocks)[activeIndex];
auto &adjacentOwner = (*blocks)[adjacentIndex];
if (activeOwner.kind != BlockKind::Paragraph
|| adjacentOwner.kind != BlockKind::Paragraph) {
return false;
}
clearTemporaryDownParagraph();
auto destinationLeaf = forward ? descriptor->leaf : adjacent->leaf;
auto seamOffset = 0;
if (forward) {
auto updated = std::move(activeOwner.text.text);
seamOffset = updated.text.size();
updated.append(std::move(adjacentOwner.text.text));
activeOwner.text.text = std::move(updated);
MergeRichTextAnchors(&activeOwner.text, std::move(adjacentOwner.text));
blocks->erase(blocks->begin() + adjacentIndex);
} else {
auto updated = std::move(adjacentOwner.text.text);
seamOffset = updated.text.size();
updated.append(std::move(activeOwner.text.text));
adjacentOwner.text.text = std::move(updated);
MergeRichTextAnchors(&adjacentOwner.text, std::move(activeOwner.text));
blocks->erase(blocks->begin() + activeIndex);
}
rebuild();
if (!activateRebuiltLeaf(destinationLeaf)) {
return false;
}
*target = {
.leaf = destinationLeaf,
.selectionFrom = seamOffset,
.selectionTo = seamOffset,
};
return true;
}
State::ParagraphBoundaryJoinResult State::joinActiveParagraphBoundary(
bool forward) {
auto failure = ParagraphBoundaryJoinResult{
.result = ApplyResult::Failed,
};
return applyCheckedMutation(failure, [forward](State &candidate) {
const auto unchanged = ParagraphBoundaryJoinResult{
.result = ApplyResult::Unchanged,
};
const auto descriptor = candidate.textNode(candidate._activeTextOrdinal);
const auto adjacent = descriptor
? candidate.adjacentTextNode(candidate._activeTextOrdinal, forward)
: nullptr;
if (!descriptor
|| !adjacent
|| descriptor->leaf.kind != LeafKind::BlockText
|| adjacent->leaf.kind != LeafKind::BlockText
|| !(descriptor->leaf.block.container
== adjacent->leaf.block.container)
|| (adjacent->leaf.block.index
!= descriptor->leaf.block.index + (forward ? 1 : -1))) {
return CheckedMutationResult<ParagraphBoundaryJoinResult>{
.result = unchanged,
};
}
const auto activeOwner = candidate.block(descriptor->leaf.block);
const auto adjacentOwner = candidate.block(adjacent->leaf.block);
if (!activeOwner
|| !adjacentOwner
|| activeOwner->kind != BlockKind::Paragraph
|| adjacentOwner->kind != BlockKind::Paragraph) {
return CheckedMutationResult<ParagraphBoundaryJoinResult>{
.result = unchanged,
};
}
ActiveTextSelectionTarget target;
if (!candidate.joinActiveParagraphBoundaryUnchecked(forward, &target)) {
return CheckedMutationResult<ParagraphBoundaryJoinResult>{
.result = {
.result = ApplyResult::Failed,
},
};
}
return CheckedMutationResult<ParagraphBoundaryJoinResult>{
.apply = true,
.result = {
.result = ApplyResult::Changed,
.destinationLeaf = target.leaf,
.selectionFrom = target.selectionFrom,
.selectionTo = target.selectionTo,
},
};
});
}
std::optional<int> State::removeStructuralSelection(
const Markdown::PreparedEditSelection &selection,
bool forward) {
clearTemporaryDownParagraph();
const auto activate = [&](const LeafPath &leaf) -> std::optional<int> {
const auto ordinal = textNodeOrdinal(leaf);
if (setActiveTextByOrdinal(ordinal)) {
return _activeTextOrdinal;
}
return std::nullopt;
};
const auto finish = [&](
auto postMutationFocus,
std::optional<LeafPath> plannedFocus) -> std::optional<int> {
rebuild();
if (const auto focus = postMutationFocus()) {
if (const auto ordinal = activate(*focus)) {
return ordinal;
}
}
if (plannedFocus) {
if (const auto ordinal = activate(*plannedFocus)) {
return ordinal;
}
}
ensureActiveTextOrdinal();
return (_activeTextOrdinal >= 0)
? std::make_optional(_activeTextOrdinal)
: std::nullopt;
};
const auto leafForContainerOwner = [&](
const BlockContainerPath &container) -> std::optional<LeafPath> {
if (container.steps.empty()) {
return std::nullopt;
}
auto parent = container;
const auto step = parent.steps.back();
parent.steps.pop_back();
const auto owner = BlockPath{
.container = parent,
.index = step.blockIndex,
};
if (step.kind == BlockContainerKind::BlockChildren) {
for (const auto &descriptor : _textNodes) {
if (leafBelongsToBlock(descriptor.leaf, owner)) {
return descriptor.leaf;
}
}
} else if (step.kind == BlockContainerKind::ListItemChildren) {
for (const auto &descriptor : _textNodes) {
const auto index = ListItemIndexForLeaf(
descriptor.leaf,
owner);
if (index && *index == step.listItemIndex) {
return descriptor.leaf;
}
}
}
return std::nullopt;
};
const auto leafNearBlockRange = [&](
const StructuralBlockRange &range,
bool forward) -> std::optional<LeafPath> {
if (forward) {
for (const auto &descriptor : _textNodes) {
const auto index = BlockIndexInContainer(
descriptor.leaf,
range.container);
if (index && *index >= range.from) {
return descriptor.leaf;
}
}
} else {
for (auto i = textNodeCount(); i != 0; --i) {
const auto &leaf = _textNodes[i - 1].leaf;
const auto index = BlockIndexInContainer(leaf, range.container);
if (index && *index < range.from) {
return leaf;
}
}
}
return leafForContainerOwner(range.container);
};
const auto leafOutsideBlock = [&](
const BlockPath &path,
bool forward) -> std::optional<LeafPath> {
if (forward) {
for (const auto &descriptor : _textNodes) {
const auto index = BlockIndexInContainer(
descriptor.leaf,
path.container);
if (index && *index > path.index) {
return descriptor.leaf;
}
}
} else {
for (auto i = textNodeCount(); i != 0; --i) {
const auto &leaf = _textNodes[i - 1].leaf;
const auto index = BlockIndexInContainer(leaf, path.container);
if (index && *index < path.index) {
return leaf;
}
}
}
return std::nullopt;
};
const auto leafNearListItemRange = [&](
const StructuralListItemRange &range,
bool forward) -> std::optional<LeafPath> {
if (forward) {
for (const auto &descriptor : _textNodes) {
const auto index = ListItemIndexForLeaf(
descriptor.leaf,
range.block);
if (index && *index >= range.from) {
return descriptor.leaf;
}
}
} else {
for (auto i = textNodeCount(); i != 0; --i) {
const auto &leaf = _textNodes[i - 1].leaf;
const auto index = ListItemIndexForLeaf(leaf, range.block);
if (index && *index < range.from) {
return leaf;
}
}
}
return leafOutsideBlock(range.block, forward);
};
const auto tableTitleLeaf = [&](
const BlockPath &path) -> std::optional<LeafPath> {
auto leaf = LeafPath{
.kind = LeafKind::BlockText,
.block = path,
};
return (textNodeOrdinal(leaf) >= 0)
? std::make_optional(leaf)
: std::nullopt;
};
const auto leafNearTableRows = [&](
const StructuralTableRowRange &range,
bool forward) -> std::optional<LeafPath> {
const auto cellInDirection = [&](
bool direction) -> std::optional<LeafPath> {
if (direction) {
for (const auto &descriptor : _textNodes) {
const auto &leaf = descriptor.leaf;
if (leaf.block == range.block
&& leaf.kind == LeafKind::TableCellText
&& leaf.tableRowIndex >= range.from) {
return leaf;
}
}
} else {
for (auto i = textNodeCount(); i != 0; --i) {
const auto &leaf = _textNodes[i - 1].leaf;
if (leaf.block == range.block
&& leaf.kind == LeafKind::TableCellText
&& leaf.tableRowIndex < range.from) {
return leaf;
}
}
}
return std::nullopt;
};
if (const auto leaf = cellInDirection(forward)) {
return leaf;
}
if (const auto leaf = cellInDirection(!forward)) {
return leaf;
}
if (const auto leaf = tableTitleLeaf(range.block)) {
return leaf;
}
return leafOutsideBlock(range.block, forward);
};
const auto leafNearTableCells = [&](
const StructuralTableCellRange &range,
bool forward) -> std::optional<LeafPath> {
if (const auto leaf = firstSelectedLeaf(range)) {
return leaf;
}
return adjacentLeafOutsideRange(range, forward);
};
const auto focusForRemovedBlock = [&](
const BlockPath &path,
bool forward) {
return leafNearBlockRange(StructuralBlockRange{
.container = path.container,
.from = path.index,
.till = path.index + 1,
}, forward);
};
switch (selection.kind) {
case PreparedEditSelectionKind::Blocks: {
const auto range = validateBlockRange(selection.blocks);
if (!range) {
return std::nullopt;
}
const auto plannedFocus = plannedFocusForRange(*range, forward);
const auto blocks = blockContainer(range->container);
if (!blocks) {
return std::nullopt;
}
blocks->erase(
blocks->begin() + range->from,
blocks->begin() + range->till);
return finish([&] {
return leafNearBlockRange(*range, forward);
}, plannedFocus);
}
case PreparedEditSelectionKind::ListItems: {
const auto range = validateListItemRange(selection.listItems);
if (!range) {
return std::nullopt;
}
const auto plannedFocus = plannedFocusForRange(*range, forward);
const auto owner = block(range->block);
if (!owner || owner->kind != BlockKind::List) {
return std::nullopt;
}
owner->listItems.erase(
owner->listItems.begin() + range->from,
owner->listItems.begin() + range->till);
if (owner->listItems.empty()) {
const auto removed = range->block;
if (!removeTarget({
.kind = RemovalKind::Block,
.block = removed,
})) {
return std::nullopt;
}
return finish([&] {
return focusForRemovedBlock(removed, forward);
}, plannedFocus);
}
return finish([&] {
return leafNearListItemRange(*range, forward);
}, plannedFocus);
}
case PreparedEditSelectionKind::TableRows: {
const auto range = validateTableRowRange(selection.tableRows);
if (!range) {
return std::nullopt;
}
const auto plannedFocus = plannedFocusForRange(*range, forward);
const auto owner = block(range->block);
if (!owner || owner->kind != BlockKind::Table) {
return std::nullopt;
}
owner->tableRows.erase(
owner->tableRows.begin() + range->from,
owner->tableRows.begin() + range->till);
if (owner->tableRows.empty() && RichTextIsEmpty(owner->text)) {
const auto removed = range->block;
if (!removeTarget({
.kind = RemovalKind::Block,
.block = removed,
})) {
return std::nullopt;
}
return finish([&] {
return focusForRemovedBlock(removed, forward);
}, plannedFocus);
}
return finish([&] {
return leafNearTableRows(*range, forward);
}, plannedFocus);
}
case PreparedEditSelectionKind::TableCells: {
const auto range = validateTableCellRange(selection.tableCells);
if (!range) {
return std::nullopt;
}
const auto owner = block(range->block);
if (!owner || owner->kind != BlockKind::Table) {
return std::nullopt;
}
const auto grid = BuildTableGrid(*owner, tableRenderLimits());
if (!TableGridRangeSpansAllRows(grid, *range)) {
return std::nullopt;
}
const auto plannedFocus = plannedFocusForRange(*range, forward);
const auto selected = SelectedTableGridCells(grid, *range);
if (selected.empty()) {
return std::nullopt;
}
auto toErase = std::vector<TableGridCellReference>();
for (const auto &reference : selected) {
if (TableGridCellContainedInRange(reference, *range)) {
toErase.push_back(reference);
continue;
}
const auto intersection = TableGridCellColumnIntersection(
reference,
*range);
if (intersection <= 0) {
continue;
}
auto &cell = owner->tableRows[reference.rowIndex].cells[
reference.cellIndex];
cell.colspan = std::max(
(reference.columnTill - reference.columnFrom) - intersection,
1);
}
std::sort(
toErase.begin(),
toErase.end(),
[](const auto &a, const auto &b) {
if (a.rowIndex != b.rowIndex) {
return a.rowIndex > b.rowIndex;
}
return a.cellIndex > b.cellIndex;
});
for (const auto &reference : toErase) {
auto &row = owner->tableRows[reference.rowIndex];
row.cells.erase(row.cells.begin() + reference.cellIndex);
}
if (std::all_of(
owner->tableRows.begin(),
owner->tableRows.end(),
[](const TableRow &row) {
return row.cells.empty();
})) {
if (RichTextIsEmpty(owner->text)) {
const auto removed = range->block;
if (!removeTarget({
.kind = RemovalKind::Block,
.block = removed,
})) {
return std::nullopt;
}
return finish([&] {
return focusForRemovedBlock(removed, forward);
}, plannedFocus);
}
auto row = TableRow();
row.cells.push_back(MakeDefaultTableCell());
owner->tableRows.clear();
owner->tableRows.push_back(std::move(row));
}
return finish([&] {
return leafNearTableCells(*range, forward);
}, plannedFocus);
}
case PreparedEditSelectionKind::None:
return std::nullopt;
}
return std::nullopt;
}
std::optional<State::BlockContainerPath> State::convertBlockContainerPath(
const Markdown::PreparedEditBlockContainerPath &path) const {
auto result = BlockContainerPath();
result.steps.reserve(path.steps.size());
const auto *blocks = &_richPage->blocks;
for (const auto &step : path.steps) {
if (step.blockIndex < 0 || step.blockIndex >= int(blocks->size())) {
return std::nullopt;
}
const auto &parent = (*blocks)[step.blockIndex];
auto converted = BlockContainerStep();
converted.blockIndex = step.blockIndex;
converted.listItemIndex = step.listItemIndex;
switch (step.kind) {
case PreparedBlockContainerKind::Root:
return std::nullopt;
case PreparedBlockContainerKind::BlockChildren:
if (!BlockCanOwnChildContainer(parent)) {
return std::nullopt;
}
converted.kind = BlockContainerKind::BlockChildren;
blocks = &parent.blocks;
break;
case PreparedBlockContainerKind::ListItemChildren:
if (parent.kind != BlockKind::List
|| step.listItemIndex < 0
|| step.listItemIndex >= int(parent.listItems.size())) {
return std::nullopt;
}
converted.kind = BlockContainerKind::ListItemChildren;
blocks = &parent.listItems[step.listItemIndex].blocks;
break;
}
result.steps.push_back(converted);
}
return result;
}
std::optional<State::BlockPath> State::convertBlockPath(
const Markdown::PreparedEditBlockPath &path) const {
if (path.index < 0) {
return std::nullopt;
}
const auto container = convertBlockContainerPath(path.container);
if (!container) {
return std::nullopt;
}
const auto blocks = blockContainer(*container);
if (!blocks || path.index >= int(blocks->size())) {
return std::nullopt;
}
return BlockPath{
.container = *container,
.index = path.index,
};
}
std::optional<State::BlockPath> State::convertBlockPath(
const Markdown::PreparedEditBlockSource &source) const {
return convertBlockPath(source.path);
}
std::optional<State::LeafPath> State::convertLeafPath(
const Markdown::PreparedEditLeafSource &source) const {
const auto blockPath = convertBlockPath(source.block);
if (!blockPath) {
return std::nullopt;
}
auto result = LeafPath();
result.block = *blockPath;
switch (source.kind) {
case PreparedEditLeafKind::BlockText:
result.kind = LeafKind::BlockText;
break;
case PreparedEditLeafKind::BlockCaption:
result.kind = LeafKind::BlockCaption;
break;
case PreparedEditLeafKind::ListItemText:
if (source.listItemIndex < 0) {
return std::nullopt;
}
result.kind = LeafKind::ListItemText;
result.listItemIndex = source.listItemIndex;
break;
case PreparedEditLeafKind::TableCellText:
if (source.tableRowIndex < 0 || source.tableCellIndex < 0) {
return std::nullopt;
}
result.kind = LeafKind::TableCellText;
result.tableRowIndex = source.tableRowIndex;
result.tableCellIndex = source.tableCellIndex;
break;
case PreparedEditLeafKind::MathFormula:
result.kind = LeafKind::MathFormula;
break;
}
const auto owner = block(result.block);
if (!owner) {
return std::nullopt;
}
switch (result.kind) {
case LeafKind::BlockText:
if (!BlockSupportsBlockText(*owner)) {
return std::nullopt;
}
break;
case LeafKind::BlockCaption:
if (!BlockSupportsBlockCaption(*owner)) {
return std::nullopt;
}
break;
case LeafKind::ListItemText:
if (owner->kind != BlockKind::List
|| !listItem(result.block, result.listItemIndex)) {
return std::nullopt;
}
break;
case LeafKind::TableCellText:
if (owner->kind != BlockKind::Table
|| !tableCell(
result.block,
result.tableRowIndex,
result.tableCellIndex)) {
return std::nullopt;
}
break;
case LeafKind::MathFormula:
if (owner->kind != BlockKind::Math) {
return std::nullopt;
}
break;
}
return result;
}
std::optional<PreparedEditLeafSource> State::convertPreparedLeafSource(
const LeafPath &path) const {
const auto owner = block(path.block);
if (!owner) {
return std::nullopt;
}
auto result = PreparedEditLeafSource();
result.block = {
.container = ToPreparedBlockContainerPath(path.block.container),
.index = path.block.index,
};
switch (path.kind) {
case LeafKind::BlockText:
if (!BlockSupportsBlockText(*owner)) {
return std::nullopt;
}
result.kind = PreparedEditLeafKind::BlockText;
break;
case LeafKind::BlockCaption:
if (!BlockSupportsBlockCaption(*owner)) {
return std::nullopt;
}
result.kind = PreparedEditLeafKind::BlockCaption;
break;
case LeafKind::ListItemText:
if (owner->kind != BlockKind::List
|| !listItem(path.block, path.listItemIndex)) {
return std::nullopt;
}
result.kind = PreparedEditLeafKind::ListItemText;
result.listItemIndex = path.listItemIndex;
break;
case LeafKind::TableCellText:
if (owner->kind != BlockKind::Table
|| !tableCell(
path.block,
path.tableRowIndex,
path.tableCellIndex)) {
return std::nullopt;
}
result.kind = PreparedEditLeafKind::TableCellText;
result.tableRowIndex = path.tableRowIndex;
result.tableCellIndex = path.tableCellIndex;
break;
case LeafKind::MathFormula:
if (owner->kind != BlockKind::Math) {
return std::nullopt;
}
result.kind = PreparedEditLeafKind::MathFormula;
break;
}
return result;
}
std::optional<PreparedEditLeafSource> State::convertPreparedLeafSource(
const TextNodeDescriptor &descriptor) const {
return convertPreparedLeafSource(descriptor.leaf);
}
std::optional<State::StructuralBlockRange> State::validateBlockRange(
const Markdown::PreparedEditBlockRange &range) const {
if (range.empty()) {
return std::nullopt;
}
const auto container = convertBlockContainerPath(range.container);
if (!container) {
return std::nullopt;
}
const auto blocks = blockContainer(*container);
if (!blocks
|| range.from < 0
|| range.till > int(blocks->size())) {
return std::nullopt;
}
for (auto i = range.from; i != range.till; ++i) {
if (!CanEditBlock((*blocks)[i])) {
return std::nullopt;
}
}
return StructuralBlockRange{
.container = *container,
.from = range.from,
.till = range.till,
};
}
std::optional<State::StructuralListItemRange> State::validateListItemRange(
const Markdown::PreparedEditListItemRange &range) const {
if (range.empty()) {
return std::nullopt;
}
const auto path = convertBlockPath(range.block);
if (!path) {
return std::nullopt;
}
const auto owner = block(*path);
if (!owner
|| owner->kind != BlockKind::List
|| range.from < 0
|| range.till > int(owner->listItems.size())) {
return std::nullopt;
}
for (auto i = range.from; i != range.till; ++i) {
if (!CanEditBlocks(owner->listItems[i].blocks)) {
return std::nullopt;
}
}
return StructuralListItemRange{
.block = *path,
.from = range.from,
.till = range.till,
};
}
std::optional<State::StructuralTableRowRange> State::validateTableRowRange(
const Markdown::PreparedEditTableRowRange &range) const {
if (range.empty()) {
return std::nullopt;
}
const auto path = convertBlockPath(range.block);
if (!path) {
return std::nullopt;
}
const auto owner = block(*path);
if (!owner
|| owner->kind != BlockKind::Table
|| range.from < 0
|| range.till > int(owner->tableRows.size())) {
return std::nullopt;
}
return StructuralTableRowRange{
.block = *path,
.from = range.from,
.till = range.till,
};
}
std::optional<State::StructuralTableCellRange> State::validateTableCellRange(
const Markdown::PreparedEditTableCellRange &range) const {
if (range.empty()) {
return std::nullopt;
}
const auto path = convertBlockPath(range.block);
if (!path) {
return std::nullopt;
}
const auto owner = block(*path);
if (!owner || owner->kind != BlockKind::Table) {
return std::nullopt;
}
const auto grid = BuildTableGrid(*owner, tableRenderLimits());
if (range.rowFrom < 0
|| range.rowTill > grid.rowCount
|| range.columnFrom < 0
|| range.columnTill > grid.columnCount) {
return std::nullopt;
}
auto result = StructuralTableCellRange{
.block = *path,
.rowFrom = range.rowFrom,
.rowTill = range.rowTill,
.columnFrom = range.columnFrom,
.columnTill = range.columnTill,
};
if (SelectedTableGridCells(grid, result).empty()) {
return std::nullopt;
}
return result;
}
bool State::leafWillBeRemoved(
const LeafPath &path,
const StructuralBlockRange &range) const {
const auto index = BlockIndexInContainer(path, range.container);
return index && IndexInRange(*index, range.from, range.till);
}
bool State::leafWillBeRemoved(
const LeafPath &path,
const StructuralListItemRange &range) const {
const auto index = ListItemIndexForLeaf(path, range.block);
return index && IndexInRange(*index, range.from, range.till);
}
bool State::leafWillBeRemoved(
const LeafPath &path,
const StructuralTableRowRange &range) const {
const auto index = TableRowIndexForLeaf(path, range.block);
return index && IndexInRange(*index, range.from, range.till);
}
bool State::leafWillBeRemoved(
const LeafPath &,
const StructuralTableCellRange &) const {
return false;
}
bool State::leafBelongsToBlock(
const LeafPath &leaf,
const BlockPath &path) const {
const auto &owner = leaf.block;
if (owner == path) {
return true;
}
if (!ContainerHasPrefix(owner.container, path.container)) {
return false;
}
if (owner.container.steps.size() <= path.container.steps.size()) {
return false;
}
const auto &step = owner.container.steps[path.container.steps.size()];
return step.blockIndex == path.index
&& (step.kind == BlockContainerKind::BlockChildren
|| step.kind == BlockContainerKind::ListItemChildren);
}
std::optional<State::LeafPath> State::firstSelectedLeaf(
const StructuralTableCellRange &range) const {
const auto owner = block(range.block);
if (!owner || owner->kind != BlockKind::Table) {
return std::nullopt;
}
const auto selected = SelectedTableGridCells(
BuildTableGrid(*owner, tableRenderLimits()),
range);
for (const auto &descriptor : _textNodes) {
const auto &leaf = descriptor.leaf;
if (leaf.block != range.block
|| leaf.kind != LeafKind::TableCellText) {
continue;
}
for (const auto &reference : selected) {
if (TableGridCellMatchesLeaf(reference, leaf, range.block)) {
return leaf;
}
}
}
return std::nullopt;
}
std::optional<State::LeafPath> State::adjacentLeafOutsideRange(
const StructuralBlockRange &range,
bool forward) const {
if (forward) {
for (const auto &descriptor : _textNodes) {
const auto index = BlockIndexInContainer(
descriptor.leaf,
range.container);
if (index && *index >= range.till) {
return descriptor.leaf;
}
}
} else {
for (auto i = textNodeCount(); i != 0; --i) {
const auto &leaf = _textNodes[i - 1].leaf;
const auto index = BlockIndexInContainer(leaf, range.container);
if (index && *index < range.from) {
return leaf;
}
}
}
return std::nullopt;
}
std::optional<State::LeafPath> State::adjacentLeafOutsideRange(
const StructuralListItemRange &range,
bool forward) const {
if (forward) {
for (const auto &descriptor : _textNodes) {
const auto index = ListItemIndexForLeaf(
descriptor.leaf,
range.block);
if (index && *index >= range.till) {
return descriptor.leaf;
}
}
} else {
for (auto i = textNodeCount(); i != 0; --i) {
const auto &leaf = _textNodes[i - 1].leaf;
const auto index = ListItemIndexForLeaf(leaf, range.block);
if (index && *index < range.from) {
return leaf;
}
}
}
return std::nullopt;
}
std::optional<State::LeafPath> State::adjacentLeafOutsideRange(
const StructuralTableRowRange &range,
bool forward) const {
if (forward) {
for (const auto &descriptor : _textNodes) {
const auto index = TableRowIndexForLeaf(
descriptor.leaf,
range.block);
if (index && *index >= range.till) {
return descriptor.leaf;
}
}
} else {
for (auto i = textNodeCount(); i != 0; --i) {
const auto &leaf = _textNodes[i - 1].leaf;
const auto index = TableRowIndexForLeaf(leaf, range.block);
if (index && *index < range.from) {
return leaf;
}
}
}
return std::nullopt;
}
std::optional<State::LeafPath> State::adjacentLeafOutsideRange(
const StructuralTableCellRange &range,
bool forward) const {
const auto owner = block(range.block);
if (!owner || owner->kind != BlockKind::Table) {
return std::nullopt;
}
const auto grid = BuildTableGrid(*owner, tableRenderLimits());
const auto leafIntersectsRange = [&](const LeafPath &leaf) {
if (leaf.block != range.block
|| leaf.kind != LeafKind::TableCellText) {
return false;
}
for (const auto &reference : grid.cells) {
if (TableGridCellMatchesLeaf(reference, leaf, range.block)) {
return TableGridCellIntersectsRange(reference, range);
}
}
return false;
};
auto fallback = std::optional<LeafPath>();
auto foundIntersecting = false;
if (forward) {
for (const auto &descriptor : _textNodes) {
if (descriptor.leaf.block == range.block
&& descriptor.leaf.kind == LeafKind::TableCellText) {
if (leafIntersectsRange(descriptor.leaf)) {
foundIntersecting = true;
} else if (foundIntersecting) {
return descriptor.leaf;
} else if (!fallback) {
fallback = descriptor.leaf;
}
}
}
} else {
for (auto i = textNodeCount(); i != 0; --i) {
const auto &leaf = _textNodes[i - 1].leaf;
if (leaf.block == range.block
&& leaf.kind == LeafKind::TableCellText) {
if (leafIntersectsRange(leaf)) {
foundIntersecting = true;
} else if (foundIntersecting) {
return leaf;
} else if (!fallback) {
fallback = leaf;
}
}
}
}
return foundIntersecting ? std::nullopt : fallback;
}
std::optional<State::LeafPath> State::fallbackFocusLeaf() const {
const auto descriptor = textNode(_activeTextOrdinal);
if (descriptor) {
return descriptor->leaf;
}
return !_textNodes.empty()
? std::make_optional(_textNodes.front().leaf)
: std::nullopt;
}
std::optional<State::LeafPath> State::plannedFocusForRange(
const StructuralBlockRange &range,
bool forward) const {
if (const auto adjacent = adjacentLeafOutsideRange(range, forward)) {
return adjacent;
}
const auto descriptor = textNode(_activeTextOrdinal);
if (descriptor && !leafWillBeRemoved(descriptor->leaf, range)) {
return descriptor->leaf;
}
for (const auto &fallback : _textNodes) {
if (!leafWillBeRemoved(fallback.leaf, range)) {
return fallback.leaf;
}
}
return fallbackFocusLeaf();
}
std::optional<State::LeafPath> State::plannedFocusForRange(
const StructuralListItemRange &range,
bool forward) const {
if (const auto adjacent = adjacentLeafOutsideRange(range, forward)) {
return adjacent;
}
const auto ownerRange = StructuralBlockRange{
.container = range.block.container,
.from = range.block.index,
.till = range.block.index + 1,
};
if (const auto adjacent = adjacentLeafOutsideRange(ownerRange, forward)) {
return adjacent;
}
const auto descriptor = textNode(_activeTextOrdinal);
if (descriptor && !leafWillBeRemoved(descriptor->leaf, range)) {
return descriptor->leaf;
}
for (const auto &fallback : _textNodes) {
if (!leafWillBeRemoved(fallback.leaf, range)) {
return fallback.leaf;
}
}
return fallbackFocusLeaf();
}
std::optional<State::LeafPath> State::plannedFocusForRange(
const StructuralTableRowRange &range,
bool forward) const {
if (const auto adjacent = adjacentLeafOutsideRange(range, forward)) {
return adjacent;
}
const auto ownerRange = StructuralBlockRange{
.container = range.block.container,
.from = range.block.index,
.till = range.block.index + 1,
};
if (const auto adjacent = adjacentLeafOutsideRange(ownerRange, forward)) {
return adjacent;
}
const auto descriptor = textNode(_activeTextOrdinal);
if (descriptor && !leafWillBeRemoved(descriptor->leaf, range)) {
return descriptor->leaf;
}
for (const auto &fallback : _textNodes) {
if (!leafWillBeRemoved(fallback.leaf, range)) {
return fallback.leaf;
}
}
return fallbackFocusLeaf();
}
std::optional<State::LeafPath> State::plannedFocusForRange(
const StructuralTableCellRange &range,
bool forward) const {
if (const auto selected = firstSelectedLeaf(range)) {
return selected;
}
if (const auto adjacent = adjacentLeafOutsideRange(range, forward)) {
return adjacent;
}
return fallbackFocusLeaf();
}
State::InsertionAnchor State::resolveActiveInsertionTarget() const {
auto result = InsertionAnchor{
.container = BlockContainerPath(),
.blockIndex = int(_richPage->blocks.size()) - 1,
};
if (const auto descriptor = textNode(_activeTextOrdinal)) {
result = descriptor->insertionAnchor;
}
return blockContainer(result.container)
? result
: InsertionAnchor{
.container = BlockContainerPath(),
.blockIndex = int(_richPage->blocks.size()) - 1,
};
}
std::optional<int> State::normalizeTextOnlyListItemForInsertion(
const BlockContainerPath &container) {
if (container.steps.empty()) {
return std::nullopt;
}
const auto &step = container.steps.back();
if (step.kind != BlockContainerKind::ListItemChildren) {
return std::nullopt;
}
auto parent = container;
parent.steps.pop_back();
auto itemPath = BlockPath{
.container = parent,
.index = step.blockIndex,
};
auto item = listItem(itemPath, step.listItemIndex);
if (!item || (item->anchorId.isEmpty() && RichTextIsEmpty(item->text))) {
return std::nullopt;
}
auto paragraph = MakeParagraphBlock();
paragraph.anchorId = std::move(item->anchorId);
paragraph.text = std::move(item->text);
item->anchorId.clear();
item->text = RichText();
if (!BlockIsEmpty(paragraph)) {
clearTemporaryDownParagraph();
item->blocks.insert(item->blocks.begin(), std::move(paragraph));
return 0;
}
return -1;
}
std::optional<int> State::normalizeTextOnlyQuoteSurface(
const BlockContainerPath &container,
bool keepEmptyParagraph) {
if (container.steps.empty()) {
return std::nullopt;
}
const auto &step = container.steps.back();
if (step.kind != BlockContainerKind::BlockChildren) {
return std::nullopt;
}
auto parent = container;
parent.steps.pop_back();
auto owner = block({
.container = parent,
.index = step.blockIndex,
});
if (!owner
|| owner->kind != BlockKind::Quote
|| !owner->blocks.empty()) {
return std::nullopt;
}
auto paragraph = MakeParagraphBlock();
paragraph.text = std::move(owner->text);
owner->text = RichText();
clearTemporaryDownParagraph();
if (keepEmptyParagraph || !BlockIsEmpty(paragraph)) {
owner->blocks.insert(owner->blocks.begin(), std::move(paragraph));
return 0;
}
return -1;
}
std::optional<int> State::normalizeTextOnlyQuoteForInsertion(
const BlockContainerPath &container) {
return normalizeTextOnlyQuoteSurface(container, false);
}
bool State::normalizeTextOnlyContainerForInsertion(
const BlockContainerPath &container,
int *insertAt) {
if (!insertAt || *insertAt < 0) {
return false;
}
if (const auto normalized = normalizeTextOnlyListItemForInsertion(
container); normalized && (*normalized >= 0)) {
++*insertAt;
}
if (const auto normalized = normalizeTextOnlyQuoteForInsertion(
container); normalized && (*normalized >= 0)) {
++*insertAt;
}
const auto blocks = blockContainer(container);
return blocks && *insertAt <= int(blocks->size());
}
bool State::shouldReplaceActiveTextOnlyBlock(
const TextNodeDescriptor &descriptor,
const std::vector<Block> &blocks) const {
if (descriptor.leaf.kind != LeafKind::BlockText
|| descriptor.removalTarget.kind != RemovalKind::Block) {
return false;
}
const auto owner = block(descriptor.removalTarget.block);
if (!owner || !BlockIsEmpty(*owner)) {
return false;
}
if (owner->kind == BlockKind::Paragraph) {
return true;
}
return owner->kind == BlockKind::Heading
&& blocks.size() == 1
&& blocks.front().kind == BlockKind::Heading;
}
std::optional<int> State::activateRebuiltLeaf(const LeafPath &path) {
const auto ordinal = textNodeOrdinal(path);
if (setActiveTextByOrdinal(ordinal)) {
return _activeTextOrdinal;
}
ensureActiveTextOrdinal();
return (_activeTextOrdinal >= 0)
? std::make_optional(_activeTextOrdinal)
: std::nullopt;
}
std::optional<State::ParagraphTarget> State::reuseOrInsertParagraph(
const BlockContainerPath &containerPath,
int index) {
const auto blocks = blockContainer(containerPath);
if (!blocks) {
return std::nullopt;
}
const auto insertAt = std::clamp(index, 0, int(blocks->size()));
if (insertAt < int(blocks->size())
&& (*blocks)[insertAt].kind == BlockKind::Paragraph) {
return ParagraphTarget{
.leaf = {
.kind = LeafKind::BlockText,
.block = {
.container = containerPath,
.index = insertAt,
},
},
};
}
clearTemporaryDownParagraph();
blocks->insert(blocks->begin() + insertAt, MakeParagraphBlock());
return ParagraphTarget{
.leaf = {
.kind = LeafKind::BlockText,
.block = {
.container = containerPath,
.index = insertAt,
},
},
.inserted = true,
};
}
auto State::resolveActiveTextInsertTarget()
-> std::optional<State::ActiveTextInsertTarget> {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor) {
return std::nullopt;
}
if (descriptor->leaf.kind == LeafKind::ListItemText) {
const auto surface = normalizeActiveListItemSurface();
if (!surface) {
return std::nullopt;
}
const auto container = ListItemChildrenContainer(
surface->path,
surface->itemIndex);
return ActiveTextInsertTarget{
.leaf = {
.kind = LeafKind::BlockText,
.block = {
.container = container,
.index = 0,
},
},
.anchor = {
.container = container,
.blockIndex = 0,
},
};
}
if (descriptor->leaf.kind == LeafKind::BlockText) {
if (const auto owner = block(descriptor->leaf.block);
owner
&& owner->kind == BlockKind::Quote
&& !owner->pullquote
&& owner->blocks.empty()) {
const auto container = BlockChildrenContainer(
descriptor->leaf.block);
if (!normalizeTextOnlyQuoteSurface(container, true)) {
return std::nullopt;
}
return ActiveTextInsertTarget{
.leaf = {
.kind = LeafKind::BlockText,
.block = {
.container = container,
.index = 0,
},
},
.anchor = {
.container = container,
.blockIndex = 0,
},
};
}
}
return ActiveTextInsertTarget{
.leaf = descriptor->leaf,
.anchor = descriptor->insertionAnchor,
};
}
auto State::activeNonPullquoteQuote() const
-> std::optional<State::ActiveNonPullquoteQuote> {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor) {
return std::nullopt;
}
const auto direct = block(descriptor->leaf.block);
if (direct && direct->kind == BlockKind::Quote) {
return direct->pullquote
? std::nullopt
: std::make_optional(ActiveNonPullquoteQuote{
.path = descriptor->leaf.block,
});
}
auto container = descriptor->leaf.block.container;
while (!container.steps.empty()) {
const auto step = container.steps.back();
container.steps.pop_back();
if (step.kind != BlockContainerKind::BlockChildren) {
continue;
}
const auto path = BlockPath{
.container = container,
.index = step.blockIndex,
};
const auto owner = block(path);
if (!owner || owner->kind != BlockKind::Quote) {
continue;
}
if (owner->pullquote) {
return std::nullopt;
}
const auto body = BlockChildrenContainer(path);
auto lastBodyLeaf = false;
for (auto i = textNodeCount(); i != 0; --i) {
const auto &candidate = _textNodes[i - 1].leaf;
if (ContainerHasPrefix(candidate.block.container, body)) {
lastBodyLeaf = (candidate == descriptor->leaf);
break;
}
}
return ActiveNonPullquoteQuote{
.path = path,
.activeLeafIsLastEditableBodyLeaf = lastBodyLeaf,
};
}
return std::nullopt;
}
std::optional<LeafPath> State::leafAfterUnwrappingBlockChildren(
const LeafPath &leaf,
const BlockPath &wrapper) const {
const auto body = BlockChildrenContainer(wrapper);
if (!ContainerHasPrefix(leaf.block.container, body)) {
return std::nullopt;
}
auto result = leaf;
if (leaf.block.container == body) {
result.block.container = wrapper.container;
result.block.index += wrapper.index;
return result;
}
auto suffix = leaf.block.container.steps;
suffix.erase(suffix.begin(), suffix.begin() + body.steps.size());
if (suffix.empty()) {
return std::nullopt;
}
suffix.front().blockIndex += wrapper.index;
result.block.container = wrapper.container;
result.block.container.steps.insert(
result.block.container.steps.end(),
suffix.begin(),
suffix.end());
return result;
}
bool State::unwrapActiveCodeBlockUnchecked(
const ActiveTextInsertContext &context,
ActiveTextSelectionTarget *target) {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor || descriptor->leaf.kind != LeafKind::BlockText) {
return false;
}
const auto owner = block(descriptor->leaf.block);
if (!owner || owner->kind != BlockKind::Code) {
return false;
}
auto *container = blockContainer(descriptor->leaf.block.container);
const auto index = descriptor->leaf.block.index;
if (!container || index < 0 || index >= int(container->size())) {
return false;
}
auto paragraph = MakeParagraphBlock();
paragraph.anchorId = owner->anchorId;
paragraph.text = owner->text;
paragraph.text.text = JoinText(
context.before,
context.selected,
context.after);
(*container)[index] = std::move(paragraph);
clearTemporaryDownParagraph();
rebuild();
if (!activateRebuiltLeaf(descriptor->leaf)) {
return false;
}
if (target) {
const auto selectionFrom = int(context.before.text.size());
*target = {
.leaf = descriptor->leaf,
.selectionFrom = selectionFrom,
.selectionTo = selectionFrom + int(context.selected.text.size()),
};
}
return true;
}
bool State::unwrapActiveBlockquoteUnchecked(
const ActiveTextInsertContext &context,
ActiveTextSelectionTarget *target) {
const auto descriptor = textNode(_activeTextOrdinal);
const auto quote = activeNonPullquoteQuote();
if (!descriptor || !quote) {
return false;
}
switch (descriptor->leaf.kind) {
case LeafKind::BlockCaption:
case LeafKind::TableCellText:
case LeafKind::MathFormula:
return false;
case LeafKind::BlockText:
case LeafKind::ListItemText:
break;
}
auto *owner = block(quote->path);
if (!owner || owner->kind != BlockKind::Quote || owner->pullquote) {
return false;
}
auto *activeText = richText(descriptor->leaf);
if (!activeText) {
return false;
}
const auto selectionFrom = int(context.before.text.size());
const auto selectionTo = selectionFrom + int(context.selected.text.size());
if (owner->blocks.empty()) {
if (descriptor->leaf.kind != LeafKind::BlockText
|| descriptor->leaf.block != quote->path
|| !RichTextIsEmpty(owner->caption)) {
return false;
}
auto *container = blockContainer(quote->path.container);
const auto index = quote->path.index;
if (!container || index < 0 || index >= int(container->size())) {
return false;
}
auto paragraph = MakeParagraphBlock();
paragraph.anchorId = owner->anchorId;
paragraph.text = owner->text;
paragraph.text.text = JoinText(
context.before,
context.selected,
context.after);
(*container)[index] = std::move(paragraph);
clearTemporaryDownParagraph();
rebuild();
if (!activateRebuiltLeaf(descriptor->leaf)) {
return false;
}
if (target) {
*target = {
.leaf = descriptor->leaf,
.selectionFrom = selectionFrom,
.selectionTo = selectionTo,
};
}
return true;
}
if (!owner->anchorId.isEmpty()
|| !RichTextIsEmpty(owner->text)
|| !RichTextIsEmpty(owner->caption)) {
return false;
}
const auto destinationLeaf = leafAfterUnwrappingBlockChildren(
descriptor->leaf,
quote->path);
if (!destinationLeaf) {
return false;
}
auto *container = blockContainer(quote->path.container);
const auto index = quote->path.index;
if (!container || index < 0 || index >= int(container->size())) {
return false;
}
activeText->text = JoinText(
context.before,
context.selected,
context.after);
auto blocks = std::move(owner->blocks);
container->erase(container->begin() + index);
container->insert(
container->begin() + index,
std::make_move_iterator(blocks.begin()),
std::make_move_iterator(blocks.end()));
clearTemporaryDownParagraph();
rebuild();
if (!activateRebuiltLeaf(*destinationLeaf)) {
return false;
}
if (target) {
*target = {
.leaf = *destinationLeaf,
.selectionFrom = selectionFrom,
.selectionTo = selectionTo,
};
}
return true;
}
auto State::activeListItemSurface() const
-> std::optional<State::ActiveListItemSurface> {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor) {
return std::nullopt;
}
auto path = std::optional<BlockPath>();
if (descriptor->leaf.kind == LeafKind::ListItemText) {
path = descriptor->leaf.block;
} else if (descriptor->leaf.kind == LeafKind::BlockText) {
const auto owner = block(descriptor->leaf.block);
if (!owner || owner->kind != BlockKind::Paragraph) {
return std::nullopt;
}
const auto &container = descriptor->leaf.block.container;
if (container.steps.empty()) {
return std::nullopt;
}
const auto &step = container.steps.back();
if (step.kind != BlockContainerKind::ListItemChildren) {
return std::nullopt;
}
auto parent = container;
parent.steps.pop_back();
path = BlockPath{
.container = parent,
.index = step.blockIndex,
};
} else {
return std::nullopt;
}
const auto owner = block(*path);
if (!owner || owner->kind != BlockKind::List) {
return std::nullopt;
}
const auto itemIndex = ListItemIndexForLeaf(descriptor->leaf, *path);
if (!itemIndex) {
return std::nullopt;
}
if (descriptor->leaf.kind == LeafKind::BlockText
&& descriptor->leaf.block.container
!= ListItemChildrenContainer(*path, *itemIndex)) {
return std::nullopt;
}
return ActiveListItemSurface{
.path = *path,
.itemIndex = *itemIndex,
};
}
auto State::normalizeActiveListItemSurface()
-> std::optional<State::ActiveListItemSurface> {
const auto descriptor = textNode(_activeTextOrdinal);
const auto surface = activeListItemSurface();
if (!descriptor
|| !surface
|| descriptor->leaf.kind != LeafKind::ListItemText) {
return surface;
}
const auto item = listItem(surface->path, surface->itemIndex);
if (!item) {
return std::nullopt;
}
auto paragraph = MakeParagraphBlock();
paragraph.anchorId = std::move(item->anchorId);
paragraph.text = std::move(item->text);
item->anchorId.clear();
item->text = RichText();
clearTemporaryDownParagraph();
item->blocks.insert(item->blocks.begin(), std::move(paragraph));
return surface;
}
RichText *State::seedInsertedBlocks(
std::vector<Block> &blocks,
TextWithEntities text) {
for (auto &block : blocks) {
if (auto target = seedInsertedBlock(block)) {
if (!text.text.isEmpty()) {
auto combined = std::move(text);
combined.append(target->text);
target->text = std::move(combined);
}
return target;
}
}
return nullptr;
}
RichText *State::seedInsertedBlock(Block &block) {
switch (block.kind) {
case BlockKind::Heading:
case BlockKind::Paragraph:
case BlockKind::Footer:
case BlockKind::Code:
case BlockKind::Table:
case BlockKind::Details:
return &block.text;
case BlockKind::Quote:
if (block.blocks.empty()) {
return &block.text;
}
for (auto &child : block.blocks) {
if (const auto result = seedInsertedBlock(child)) {
return result;
}
}
return &block.caption;
case BlockKind::List:
for (auto &item : block.listItems) {
if (!RichTextIsEmpty(item.text) || item.blocks.empty()) {
return &item.text;
}
for (auto &child : item.blocks) {
if (const auto result = seedInsertedBlock(child)) {
return result;
}
}
}
return nullptr;
case BlockKind::Photo:
case BlockKind::Video:
case BlockKind::Audio:
case BlockKind::Map:
return &block.caption;
default:
return nullptr;
}
}
bool State::appendInsertedTrailingText(
const BlockContainerPath &container,
int insertAt,
int count,
TextWithEntities text) {
if (text.text.isEmpty()) {
return true;
}
const auto blocks = blockContainer(container);
if (!blocks
|| insertAt < 0
|| count < 0
|| insertAt + count > int(blocks->size())) {
return false;
}
auto &last = (*blocks)[insertAt + count - 1];
if (last.kind == BlockKind::List) {
const auto taskState = last.listItems.empty()
? TaskState::None
: last.listItems.back().taskState;
auto item = MakeParagraphListItem(taskState);
Assert(!item.blocks.empty());
item.blocks.front().text.text = std::move(text);
last.listItems.push_back(std::move(item));
return true;
}
const auto paragraph = reuseOrInsertParagraph(container, insertAt + count);
if (!paragraph) {
return false;
}
const auto target = richText(paragraph->leaf);
if (!target) {
return false;
}
target->text = std::move(text);
return true;
}
std::optional<int> State::ensureTrailingParagraphActive() {
return applyCheckedMutation(std::optional<int>(), [](State &candidate) {
const auto result = candidate.ensureTrailingParagraphActiveUnchecked();
return CheckedMutationResult<std::optional<int>>{
.apply = result.has_value(),
.result = result,
};
});
}
std::optional<int> State::ensureTrailingParagraphActiveUnchecked() {
if (_richPage->blocks.empty()
|| _richPage->blocks.back().kind != BlockKind::Paragraph) {
clearTemporaryDownParagraph();
_richPage->blocks.push_back(MakeParagraphBlock());
}
const auto path = BlockPath{
.container = BlockContainerPath(),
.index = int(_richPage->blocks.size()) - 1,
};
rebuild();
const auto ordinal = textNodeOrdinal({
.kind = LeafKind::BlockText,
.block = path,
});
if (!setActiveTextByOrdinal(ordinal)) {
ensureActiveTextOrdinal();
}
return (_activeTextOrdinal >= 0)
? std::make_optional(_activeTextOrdinal)
: std::nullopt;
}
std::optional<int> State::insertLeadingParagraphActive(bool focusInserted) {
return applyCheckedMutation(std::optional<int>(), [=](State &candidate) {
const auto result = candidate.insertLeadingParagraphActiveUnchecked(
focusInserted);
return CheckedMutationResult<std::optional<int>>{
.apply = result.has_value(),
.result = result,
};
});
}
std::optional<int> State::insertLeadingParagraphActiveUnchecked(
bool focusInserted) {
auto restore = std::optional<LeafPath>();
if (!focusInserted) {
if (const auto descriptor = textNode(_activeTextOrdinal)) {
restore = descriptor->leaf;
// The paragraph is prepended to the top-level blocks list,
// so the active leaf's root-level index shifts by one.
if (restore->block.container.steps.empty()) {
++restore->block.index;
} else {
++restore->block.container.steps.front().blockIndex;
}
}
}
clearTemporaryDownParagraph();
_richPage->blocks.insert(_richPage->blocks.begin(), MakeParagraphBlock());
rebuild();
const auto inserted = LeafPath{
.kind = LeafKind::BlockText,
.block = {
.container = BlockContainerPath(),
.index = 0,
},
};
const auto target = restore.value_or(inserted);
if (!setActiveTextByOrdinal(textNodeOrdinal(target))) {
ensureActiveTextOrdinal();
}
return (_activeTextOrdinal >= 0)
? std::make_optional(_activeTextOrdinal)
: std::nullopt;
}
void State::resyncAfterExternalRichPageMutation() {
clearTemporaryDownParagraph();
const auto activeLeaf = [&]() -> std::optional<LeafPath> {
if (const auto descriptor = textNode(_activeTextOrdinal)) {
return descriptor->leaf;
}
return std::nullopt;
}();
rebuild();
if (activeLeaf && (textNodeOrdinal(*activeLeaf) >= 0)) {
const auto activated = activateRebuiltLeaf(*activeLeaf);
Assert(activated);
} else {
ensureActiveTextOrdinal();
}
}
std::optional<int> State::moveActiveSpecialBlockDown() {
return applyCheckedMutation(std::optional<int>(), [](State &candidate) {
const auto result = candidate.moveActiveSpecialBlockDownUnchecked();
return CheckedMutationResult<std::optional<int>>{
.apply = result.has_value(),
.result = result,
};
});
}
std::optional<int> State::submitActiveSingleLineField() {
return applyCheckedMutation(std::optional<int>(), [](State &candidate) {
const auto result = candidate.submitActiveSingleLineFieldUnchecked();
return CheckedMutationResult<std::optional<int>>{
.apply = result.has_value(),
.result = result,
};
});
}
std::optional<int> State::escapeActiveBlockBody() {
return applyCheckedMutation(std::optional<int>(), [](State &candidate) {
const auto result = candidate.escapeActiveBlockBodyUnchecked();
return CheckedMutationResult<std::optional<int>>{
.apply = result.has_value(),
.result = result,
};
});
}
State::BoundaryTarget State::removeTemporaryDownParagraphAndMove() {
return applyCheckedMutation(BoundaryTarget(), [](State &candidate) {
const auto result
= candidate.removeTemporaryDownParagraphAndMoveUnchecked();
return CheckedMutationResult<BoundaryTarget>{
.apply = (result.action != BoundaryAction::None),
.result = result,
};
});
}
std::optional<int> State::moveActiveSpecialBlockDownUnchecked() {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor) {
return std::nullopt;
}
auto target = std::optional<LeafPath>();
auto trackTemporary = false;
if (const auto quote = activeNonPullquoteQuote()) {
if (descriptor->leaf.kind == LeafKind::BlockCaption
&& descriptor->leaf.block == quote->path) {
if (const auto paragraph = reuseOrInsertParagraph(
quote->path.container,
quote->path.index + 1)) {
target = paragraph->leaf;
trackTemporary = paragraph->inserted;
}
} else if (descriptor->leaf.kind == LeafKind::BlockText
&& descriptor->leaf.block == quote->path) {
const auto owner = block(quote->path);
if (owner
&& owner->kind == BlockKind::Quote
&& owner->blocks.empty()) {
target = LeafPath{
.kind = LeafKind::BlockCaption,
.block = quote->path,
};
}
} else if (quote->activeLeafIsLastEditableBodyLeaf) {
target = LeafPath{
.kind = LeafKind::BlockCaption,
.block = quote->path,
};
}
} else if (descriptor->leaf.kind == LeafKind::BlockText) {
const auto owner = block(descriptor->leaf.block);
if (owner && owner->kind == BlockKind::Code) {
if (const auto paragraph = reuseOrInsertParagraph(
descriptor->leaf.block.container,
descriptor->leaf.block.index + 1)) {
target = paragraph->leaf;
trackTemporary = paragraph->inserted;
}
}
}
if (!target) {
return std::nullopt;
}
_temporaryDownParagraph = trackTemporary
? std::make_optional(*target)
: std::nullopt;
rebuild();
return activateRebuiltLeaf(*target);
}
std::optional<int> State::submitActiveSingleLineFieldUnchecked() {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor) {
return std::nullopt;
}
const auto leaf = descriptor->leaf;
const auto owner = block(leaf.block);
if (!owner) {
return std::nullopt;
}
const auto activate = [&](const LeafPath &target) -> std::optional<int> {
const auto ordinal = textNodeOrdinal(target);
return setActiveTextByOrdinal(ordinal)
? std::make_optional(_activeTextOrdinal)
: std::nullopt;
};
const auto paragraphAfterBlock = [&]() -> std::optional<int> {
if (const auto paragraph = reuseOrInsertParagraph(
leaf.block.container,
leaf.block.index + 1)) {
rebuild();
return activateRebuiltLeaf(paragraph->leaf);
}
return std::nullopt;
};
if (leaf.kind == LeafKind::BlockCaption) {
switch (owner->kind) {
case BlockKind::Quote:
case BlockKind::Photo:
case BlockKind::Video:
case BlockKind::Audio:
case BlockKind::Map:
return paragraphAfterBlock();
default:
return std::nullopt;
}
}
if (leaf.kind == LeafKind::BlockText) {
if (owner->kind == BlockKind::Details) {
const auto bodyContainer = BlockChildrenContainer(leaf.block);
auto target = std::optional<LeafPath>();
for (const auto &candidate : _textNodes) {
if (ContainerStartsWith(
candidate.leaf.block.container,
bodyContainer)) {
target = candidate.leaf;
break;
}
}
if (!target) {
owner->blocks.push_back(MakeParagraphBlock());
target = LeafPath{
.kind = LeafKind::BlockText,
.block = {
.container = bodyContainer,
.index = 0,
},
};
rebuild();
return activateRebuiltLeaf(*target);
}
return activate(*target);
} else if (owner->kind == BlockKind::Table) {
for (const auto &candidate : _textNodes) {
if (candidate.leaf.block == leaf.block
&& candidate.leaf.kind == LeafKind::TableCellText) {
return activate(candidate.leaf);
}
}
return paragraphAfterBlock();
}
return std::nullopt;
} else if (leaf.kind == LeafKind::TableCellText) {
const auto ordinal = textNodeOrdinal(leaf);
for (auto i = ordinal + 1, count = textNodeCount(); i != count; ++i) {
const auto &candidate = _textNodes[i].leaf;
if (candidate.block == leaf.block
&& candidate.kind == LeafKind::TableCellText) {
return activate(candidate);
}
}
return paragraphAfterBlock();
}
return std::nullopt;
}
std::optional<int> State::escapeActiveBlockBodyUnchecked() {
const auto targetBlock = activeBlockBodyEscapeBlock();
if (!targetBlock) {
return std::nullopt;
}
if (const auto paragraph = reuseOrInsertParagraph(
targetBlock->container,
targetBlock->index + 1)) {
rebuild();
return activateRebuiltLeaf(paragraph->leaf);
}
return std::nullopt;
}
std::optional<State::BlockPath> State::activeBlockBodyEscapeBlock() const {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor) {
return std::nullopt;
}
const auto leaf = descriptor->leaf;
auto targetBlock = std::optional<BlockPath>();
if (const auto owner = block(leaf.block);
owner
&& leaf.kind == LeafKind::BlockText
&& (owner->kind == BlockKind::Quote
|| owner->kind == BlockKind::Code)) {
targetBlock = leaf.block;
} else {
auto container = leaf.block.container;
while (!container.steps.empty()) {
const auto step = container.steps.back();
container.steps.pop_back();
if (step.kind != BlockContainerKind::BlockChildren) {
continue;
}
const auto candidate = BlockPath{
.container = container,
.index = step.blockIndex,
};
const auto owner = block(candidate);
if (owner
&& (owner->kind == BlockKind::Quote
|| owner->kind == BlockKind::Details)) {
targetBlock = candidate;
break;
}
}
}
return targetBlock;
}
auto State::captureRebuiltBoundaryTarget(
const BoundaryTarget &target) const
-> std::optional<RebuiltBoundaryTarget> {
switch (target.action) {
case BoundaryAction::Text:
if (const auto descriptor = textNode(target.textOrdinal)) {
return RebuiltBoundaryTarget{
.action = BoundaryAction::Text,
.leaf = descriptor->leaf,
};
}
break;
case BoundaryAction::StructuralSelection:
switch (target.structuralSelection.kind) {
case PreparedEditSelectionKind::Blocks:
if (const auto range = validateBlockRange(
target.structuralSelection.blocks);
range
&& (range->till == range->from + 1)) {
return RebuiltBoundaryTarget{
.action = BoundaryAction::StructuralSelection,
.block = {
.container = range->container,
.index = range->from,
},
};
}
break;
case PreparedEditSelectionKind::ListItems:
if (const auto range = validateListItemRange(
target.structuralSelection.listItems);
range
&& (range->till == range->from + 1)) {
return RebuiltBoundaryTarget{
.action = BoundaryAction::StructuralSelection,
.block = range->block,
.listItemIndex = range->from,
};
}
break;
default:
break;
}
break;
default:
break;
}
return std::nullopt;
}
void State::shiftRebuiltBoundaryTargetAfterRemovedBlock(
RebuiltBoundaryTarget &target,
const BlockPath &removed) const {
switch (target.action) {
case BoundaryAction::Text:
if (!ShiftBlockPathAfterRemovedBlock(target.leaf.block, removed)) {
target = RebuiltBoundaryTarget();
}
break;
case BoundaryAction::StructuralSelection:
if (!ShiftBlockPathAfterRemovedBlock(target.block, removed)) {
target = RebuiltBoundaryTarget();
}
break;
default:
target = RebuiltBoundaryTarget();
break;
}
}
State::BoundaryTarget State::materializeBoundaryTarget(
const RebuiltBoundaryTarget &target) const {
switch (target.action) {
case BoundaryAction::Text:
if (const auto ordinal = textNodeOrdinal(target.leaf); ordinal >= 0) {
return {
.action = BoundaryAction::Text,
.textOrdinal = ordinal,
};
}
break;
case BoundaryAction::StructuralSelection:
if (target.listItemIndex >= 0) {
const auto owner = block(target.block);
if (owner
&& owner->kind == BlockKind::List
&& target.listItemIndex < int(owner->listItems.size())
&& CanEditBlocks(owner->listItems[target.listItemIndex].blocks)) {
return {
.action = BoundaryAction::StructuralSelection,
.structuralSelection = preparedSelectionForListItem(
target.block,
target.listItemIndex),
};
}
} else if (const auto owner = block(target.block);
owner && CanEditBlock(*owner)) {
return {
.action = BoundaryAction::StructuralSelection,
.structuralSelection = preparedSelectionForBlock(target.block),
};
}
break;
default:
break;
}
return {};
}
State::BoundaryTarget State::removeTemporaryDownParagraphAndMoveUnchecked() {
const auto tracked = _temporaryDownParagraph;
const auto descriptor = textNode(_activeTextOrdinal);
if (!tracked
|| !descriptor
|| !(descriptor->leaf == *tracked)) {
return {};
}
const auto owner = block(tracked->block);
if (!owner || owner->kind != BlockKind::Paragraph) {
clearTemporaryDownParagraph();
return {};
}
if (!BlockIsEmpty(*owner)) {
clearTemporaryDownParagraph();
return {};
}
const auto next = boundaryTargetForLeaf(
*tracked,
descriptor,
true,
false);
if (next.action == BoundaryAction::None) {
return {};
}
auto rebuiltTarget = captureRebuiltBoundaryTarget(next);
if (!rebuiltTarget) {
return {};
}
const auto removed = tracked->block;
if (!removeTarget({
.kind = RemovalKind::Block,
.block = removed,
})) {
return {};
}
shiftRebuiltBoundaryTargetAfterRemovedBlock(*rebuiltTarget, removed);
if (rebuiltTarget->action == BoundaryAction::None) {
return {};
}
rebuild();
const auto materialized = materializeBoundaryTarget(*rebuiltTarget);
if (materialized.action == BoundaryAction::Text) {
if (!setActiveTextByOrdinal(materialized.textOrdinal)) {
ensureActiveTextOrdinal();
}
}
return materialized;
}
std::optional<int> State::handleActiveHeadingEnter() {
return applyCheckedMutation(std::optional<int>(), [](State &candidate) {
const auto result = candidate.handleActiveHeadingEnterUnchecked();
return CheckedMutationResult<std::optional<int>>{
.apply = result.has_value(),
.result = result,
};
});
}
std::optional<int> State::handleActiveHeadingEnterUnchecked() {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor || descriptor->leaf.kind != LeafKind::BlockText) {
return std::nullopt;
}
const auto path = descriptor->leaf.block;
const auto blocks = blockContainer(path.container);
if (!blocks
|| path.index < 0
|| path.index >= int(blocks->size())
|| (*blocks)[path.index].kind != BlockKind::Heading) {
return std::nullopt;
}
const auto insertAt = path.index + 1;
if (insertAt < 0 || insertAt > int(blocks->size())) {
return std::nullopt;
}
clearTemporaryDownParagraph();
blocks->insert(blocks->begin() + insertAt, MakeParagraphBlock());
const auto target = LeafPath{
.kind = LeafKind::BlockText,
.block = {
.container = path.container,
.index = insertAt,
},
};
rebuild();
return activateRebuiltLeaf(target);
}
std::optional<int> State::handleActiveListEnter() {
return applyCheckedMutation(std::optional<int>(), [](State &candidate) {
const auto result = candidate.handleActiveListEnterUnchecked();
return CheckedMutationResult<std::optional<int>>{
.apply = result.has_value(),
.result = result,
};
});
}
std::optional<int> State::handleActiveListEnterUnchecked() {
const auto surface = normalizeActiveListItemSurface();
if (!surface) {
return std::nullopt;
}
const auto owner = block(surface->path);
const auto item = listItem(surface->path, surface->itemIndex);
if (!owner || owner->kind != BlockKind::List || !item) {
return std::nullopt;
}
auto target = std::optional<LeafPath>();
const auto trailingEmpty = (surface->itemIndex + 1
== int(owner->listItems.size()))
&& (item->blocks.size() == 1)
&& (item->blocks.front().kind == BlockKind::Paragraph)
&& ListItemIsEmpty(*item);
if (trailingEmpty) {
clearTemporaryDownParagraph();
owner->listItems.erase(owner->listItems.begin() + surface->itemIndex);
if (owner->listItems.empty()) {
const auto blocks = blockContainer(surface->path.container);
if (!blocks
|| surface->path.index < 0
|| surface->path.index >= int(blocks->size())) {
return std::nullopt;
}
clearTemporaryDownParagraph();
blocks->erase(blocks->begin() + surface->path.index);
if (const auto paragraph = reuseOrInsertParagraph(
surface->path.container,
surface->path.index)) {
target = paragraph->leaf;
}
} else {
if (const auto paragraph = reuseOrInsertParagraph(
surface->path.container,
surface->path.index + 1)) {
target = paragraph->leaf;
}
}
} else {
clearTemporaryDownParagraph();
owner->listItems.insert(
owner->listItems.begin() + surface->itemIndex + 1,
MakeParagraphListItem(item->taskState));
target = LeafPath{
.kind = LeafKind::BlockText,
.block = {
.container = ListItemChildrenContainer(
surface->path,
surface->itemIndex + 1),
.index = 0,
},
};
}
if (!target) {
return std::nullopt;
}
rebuild();
return activateRebuiltLeaf(*target);
}
bool State::pasteClipboardListItemsAfterActive(
const ClipboardListItemsData &data,
std::optional<ActiveTextInsertContext> context) {
if (data.items.empty()) {
return false;
}
return applyCheckedMutation(false, [
data,
context = std::move(context)](State &candidate) mutable {
auto block = Block();
block.kind = BlockKind::List;
block.listKind = data.listKind;
block.orderedList = data.orderedList;
block.listItems = data.items;
auto blocks = std::vector<Block>();
blocks.push_back(std::move(block));
NormalizeInsertedOrderedListMetadata(&blocks);
candidate.normalizeInsertedBlockAnchors(blocks);
const auto sameList = [&] {
const auto descriptor = candidate.textNode(
candidate._activeTextOrdinal);
const auto surface = candidate.activeListItemSurface();
auto owner = surface ? candidate.block(surface->path) : nullptr;
if (!descriptor
|| !surface
|| !owner
|| !ListBlockMatchesClipboardData(*owner, data)) {
return false;
}
auto insertContext = context
? *context
: ActiveTextInsertContext{
.before = candidate.activeText(),
};
const auto itemContainer = ListItemChildrenContainer(
surface->path,
surface->itemIndex);
auto activeBlockIndex = -1;
switch (descriptor->leaf.kind) {
case LeafKind::ListItemText:
break;
case LeafKind::BlockText:
if (descriptor->leaf.block.container != itemContainer) {
return false;
}
activeBlockIndex = descriptor->leaf.block.index;
break;
default:
return false;
}
if (const auto normalized
= candidate.normalizeTextOnlyListItemForInsertion(
itemContainer)) {
if (descriptor->leaf.kind == LeafKind::ListItemText) {
activeBlockIndex = *normalized;
} else if (*normalized >= 0) {
++activeBlockIndex;
}
} else if (descriptor->leaf.kind == LeafKind::ListItemText) {
activeBlockIndex = -1;
}
owner = candidate.block(surface->path);
if (!owner
|| owner->kind != BlockKind::List
|| surface->itemIndex < 0
|| surface->itemIndex >= int(owner->listItems.size())) {
return false;
}
const auto item = &owner->listItems[surface->itemIndex];
if (activeBlockIndex >= 0) {
if (activeBlockIndex >= int(item->blocks.size())
|| item->blocks[activeBlockIndex].kind
!= BlockKind::Paragraph) {
return false;
}
} else if (!item->blocks.empty()) {
return false;
}
enum class OriginalParagraphSide {
None,
Leading,
Trailing,
};
enum class OriginalItemSide {
None,
Leading,
Trailing,
};
const auto makeParagraph = [](TextWithEntities text) {
auto paragraph = State::MakeParagraphBlock();
paragraph.text.text = std::move(text);
return paragraph;
};
candidate.clearTemporaryDownParagraph();
auto current = std::move(owner->listItems[surface->itemIndex]);
auto insertedItems = std::move(blocks.front().listItems);
const auto insertedCount = int(insertedItems.size());
auto leading = ListItem();
leading.taskState = current.taskState;
auto trailing = ListItem();
trailing.taskState = current.taskState;
auto activeParagraph = Block();
if (activeBlockIndex >= 0) {
activeParagraph = std::move(current.blocks[activeBlockIndex]);
}
for (auto i = 0; i < std::max(activeBlockIndex, 0); ++i) {
leading.blocks.push_back(std::move(current.blocks[i]));
}
auto originalParagraphSide = OriginalParagraphSide::None;
if (activeBlockIndex >= 0 && !insertContext.before.text.isEmpty()) {
activeParagraph.text.text = std::move(insertContext.before);
leading.blocks.push_back(std::move(activeParagraph));
originalParagraphSide = OriginalParagraphSide::Leading;
}
if (activeBlockIndex >= 0 && !insertContext.after.text.isEmpty()) {
if (originalParagraphSide == OriginalParagraphSide::None) {
activeParagraph.text.text = std::move(insertContext.after);
trailing.blocks.push_back(std::move(activeParagraph));
originalParagraphSide = OriginalParagraphSide::Trailing;
} else {
trailing.blocks.push_back(makeParagraph(
std::move(insertContext.after)));
}
}
if (activeBlockIndex >= 0) {
for (auto i = activeBlockIndex + 1;
i < int(current.blocks.size());
++i) {
trailing.blocks.push_back(std::move(current.blocks[i]));
}
}
auto originalItemSide = OriginalItemSide::None;
switch (originalParagraphSide) {
case OriginalParagraphSide::Leading:
originalItemSide = OriginalItemSide::Leading;
break;
case OriginalParagraphSide::Trailing:
originalItemSide = OriginalItemSide::Trailing;
break;
case OriginalParagraphSide::None:
break;
}
const auto keepLeading = !State::ListItemIsEmpty(leading);
const auto keepTrailing = !State::ListItemIsEmpty(trailing);
if (originalItemSide == OriginalItemSide::None) {
if (keepLeading) {
originalItemSide = OriginalItemSide::Leading;
} else if (keepTrailing) {
originalItemSide = OriginalItemSide::Trailing;
}
}
if (originalItemSide == OriginalItemSide::Leading) {
leading.number = std::move(current.number);
leading.anchorId = std::move(current.anchorId);
leading.text = std::move(current.text);
} else if (originalItemSide == OriginalItemSide::Trailing) {
trailing.number = std::move(current.number);
trailing.anchorId = std::move(current.anchorId);
trailing.text = std::move(current.text);
}
auto replacement = std::vector<ListItem>();
replacement.reserve(
insertedCount
+ (keepLeading ? 1 : 0)
+ (keepTrailing ? 1 : 0));
if (keepLeading) {
replacement.push_back(std::move(leading));
}
const auto insertedFrom = surface->itemIndex + int(keepLeading);
replacement.insert(
replacement.end(),
std::make_move_iterator(insertedItems.begin()),
std::make_move_iterator(insertedItems.end()));
if (keepTrailing) {
replacement.push_back(std::move(trailing));
}
owner->listItems.erase(owner->listItems.begin() + surface->itemIndex);
owner->listItems.insert(
owner->listItems.begin() + surface->itemIndex,
std::make_move_iterator(replacement.begin()),
std::make_move_iterator(replacement.end()));
candidate.rebuild();
for (auto i = 0, count = candidate.textNodeCount(); i != count; ++i) {
const auto itemIndex = ListItemIndexForLeaf(
candidate._textNodes[i].leaf,
surface->path);
if (itemIndex
&& (*itemIndex >= insertedFrom)
&& (*itemIndex < insertedFrom + insertedCount)
&& candidate.setActiveTextByOrdinal(i)) {
return true;
}
}
candidate.ensureActiveTextOrdinal();
return true;
}();
const auto applied = sameList
? true
: candidate.insertBlocksAfterActiveUnchecked(
std::move(blocks),
std::move(context));
return CheckedMutationResult<bool>{
.apply = applied,
.result = applied,
};
});
}
bool State::wrapStructuralBlockSelection(
const Markdown::PreparedEditSelection &selection,
InsertAction action,
BoundaryTarget *destination) {
if (selection.kind != PreparedEditSelectionKind::Blocks) {
return false;
}
const auto range = validateBlockRange(selection.blocks);
auto payload = structuredClipboardDataForSelection(selection);
auto *data = payload
? std::get_if<ClipboardBlockData>(&*payload)
: nullptr;
if (!range || !data) {
return false;
}
auto container = range->container;
auto insertAt = range->from;
if (!removeStructuralSelection(selection, true)) {
return false;
}
if (const auto normalized = normalizeTextOnlyListItemForInsertion(
container); normalized && (*normalized >= 0)) {
++insertAt;
}
auto wrapper = makeBlock(action);
switch (action.type) {
case InsertBlockType::Blockquote:
case InsertBlockType::Pullquote:
case InsertBlockType::Details:
wrapper.blocks = std::move(data->blocks);
break;
case InsertBlockType::OrderedList:
case InsertBlockType::BulletList:
if (wrapper.kind != BlockKind::List || wrapper.listItems.empty()) {
return false;
}
wrapper.listItems.front().blocks = std::move(data->blocks);
adoptLeadingParagraphListItemText(&wrapper.listItems.front());
break;
default:
return false;
}
auto inserted = std::vector<Block>();
inserted.push_back(std::move(wrapper));
normalizeInsertedBlockAnchors(inserted);
if (!insertPreparedBlocksAtExplicitPosition(
std::move(inserted),
container,
&insertAt)) {
return false;
}
rebuild();
const auto target = destinationTargetForInsertedBlocks(
container,
insertAt,
1);
if (destination) {
*destination = target;
}
return true;
}
bool State::unwrapMatchingStructuralWrapper(
const Markdown::PreparedEditSelection &selection,
InsertBlockType type,
BoundaryTarget *destination) {
if (selection.kind != PreparedEditSelectionKind::Blocks) {
return false;
}
const auto range = validateBlockRange(selection.blocks);
if (!range || range->container.steps.empty()) {
return false;
}
const auto step = range->container.steps.back();
if (step.kind != BlockContainerKind::BlockChildren) {
return false;
}
auto parentContainer = range->container;
parentContainer.steps.pop_back();
auto *parent = blockContainer(parentContainer);
const auto wrapperPath = BlockPath{
.container = parentContainer,
.index = step.blockIndex,
};
auto *wrapper = block(wrapperPath);
const auto matches = [&](const Block &block) {
switch (type) {
case InsertBlockType::Blockquote:
return (block.kind == BlockKind::Quote) && !block.pullquote;
case InsertBlockType::Pullquote:
return (block.kind == BlockKind::Quote) && block.pullquote;
case InsertBlockType::Details:
return (block.kind == BlockKind::Details);
default:
return false;
}
};
if (!parent
|| !wrapper
|| !matches(*wrapper)
|| (range->from != 0)
|| (range->till != int(wrapper->blocks.size()))) {
return false;
}
if (!wrapper->anchorId.isEmpty()
|| !RichTextIsEmpty(wrapper->text)
|| !RichTextIsEmpty(wrapper->caption)) {
return false;
}
clearTemporaryDownParagraph();
auto blocks = std::move(wrapper->blocks);
const auto insertedCount = int(blocks.size());
parent->erase(parent->begin() + wrapperPath.index);
parent->insert(
parent->begin() + wrapperPath.index,
std::make_move_iterator(blocks.begin()),
std::make_move_iterator(blocks.end()));
rebuild();
const auto target = destinationTargetForInsertedBlocks(
parentContainer,
wrapperPath.index,
insertedCount);
if (destination) {
*destination = target;
}
return true;
}
std::vector<Block> State::takeListItemBlocksForUnwrap(ListItem *item) {
auto result = std::vector<Block>();
if (!item) {
return result;
}
if (!item->anchorId.isEmpty() || !RichTextIsEmpty(item->text)) {
auto paragraph = MakeParagraphBlock();
paragraph.anchorId = std::move(item->anchorId);
paragraph.text = std::move(item->text);
result.push_back(std::move(paragraph));
}
result.insert(
result.end(),
std::make_move_iterator(item->blocks.begin()),
std::make_move_iterator(item->blocks.end()));
item->blocks.clear();
return result;
}
void State::adoptLeadingParagraphListItemText(ListItem *item) const {
if (!item
|| item->blocks.empty()
|| item->blocks.front().kind != BlockKind::Paragraph) {
return;
}
item->text = std::move(item->blocks.front().text);
item->anchorId = std::move(item->blocks.front().anchorId);
item->blocks.erase(item->blocks.begin());
}
bool State::unwrapMatchingListItemWrapper(
const Markdown::PreparedEditSelection &selection,
InsertBlockType type,
BoundaryTarget *destination) {
if (selection.kind != PreparedEditSelectionKind::ListItems) {
return false;
}
const auto range = validateListItemRange(selection.listItems);
if (!range || (range->from + 1 != range->till)) {
return false;
}
auto *owner = block(range->block);
auto *parent = blockContainer(range->block.container);
const auto matches = [&](const Block &block) {
switch (type) {
case InsertBlockType::OrderedList:
return (block.kind == BlockKind::List)
&& (block.listKind == ListKind::Ordered);
case InsertBlockType::BulletList:
return (block.kind == BlockKind::List)
&& (block.listKind == ListKind::Bullet);
default:
return false;
}
};
if (!owner || !parent || !matches(*owner) || IsTaskList(owner->listItems)) {
return false;
}
clearTemporaryDownParagraph();
const auto hasLeading = (range->from > 0);
const auto hasTrailing = (range->till < int(owner->listItems.size()));
const auto leadingStart = (owner->listKind == ListKind::Ordered
&& hasLeading)
? EffectiveOrderedItemValue(*owner, 0)
: std::optional<int>();
const auto trailingStart = (owner->listKind == ListKind::Ordered
&& hasTrailing)
? EffectiveOrderedItemValue(*owner, range->till)
: std::optional<int>();
auto inserted = takeListItemBlocksForUnwrap(&owner->listItems[range->from]);
auto trailing = std::optional<Block>();
if (hasTrailing) {
trailing = Block();
trailing->kind = BlockKind::List;
trailing->listKind = owner->listKind;
trailing->orderedList = owner->orderedList;
if (trailingStart.has_value()) {
trailing->orderedList.start = trailingStart;
}
trailing->listItems = std::vector<ListItem>(
std::make_move_iterator(owner->listItems.begin() + range->till),
std::make_move_iterator(owner->listItems.end()));
}
if (hasLeading) {
owner->listItems.erase(
owner->listItems.begin() + range->from,
owner->listItems.end());
if (leadingStart.has_value()) {
owner->orderedList.start = leadingStart;
}
} else {
parent->erase(parent->begin() + range->block.index);
}
auto insertAt = range->block.index + (hasLeading ? 1 : 0);
const auto insertedCount = int(inserted.size());
parent->insert(
parent->begin() + insertAt,
std::make_move_iterator(inserted.begin()),
std::make_move_iterator(inserted.end()));
if (trailing.has_value()) {
NormalizeInsertedOrderedListMetadata(&*trailing);
parent->insert(
parent->begin() + insertAt + insertedCount,
std::move(*trailing));
}
rebuild();
const auto target = destinationTargetForInsertedBlocks(
range->block.container,
insertAt,
insertedCount);
if (destination) {
*destination = target;
}
return true;
}
bool State::replaceStructuralSelectionWithBlock(
const Markdown::PreparedEditSelection &selection,
InsertAction action,
std::optional<ActiveTextInsertContext> context,
BoundaryTarget *destination) {
_lastLimitError = std::nullopt;
if (destination) {
*destination = {};
}
auto candidate = State(
std::make_shared<RichPage>(*_richPage),
_mediaRuntime,
_limits);
candidate._activeTextOrdinal = _activeTextOrdinal;
candidate._lastLimitError = std::nullopt;
candidate._temporaryDownParagraph = _temporaryDownParagraph;
const auto commitValidatedCandidate = [&](State &&candidate) {
const auto error = ValidateRichMessage(
*candidate._richPage,
_limits);
if (error) {
_lastLimitError = error;
return false;
}
commitCheckedMutation(std::move(candidate));
return true;
};
const auto structuralTextBlockConversion = [&]()
-> std::optional<LeafPath> {
const auto allowed = [](InsertBlockType type) {
switch (type) {
case InsertBlockType::Heading:
case InsertBlockType::Code:
return true;
default:
return false;
}
};
if (selection.kind != PreparedEditSelectionKind::Blocks
|| !allowed(action.type)) {
return std::nullopt;
}
const auto range = candidate.validateBlockRange(selection.blocks);
if (!range || (range->from + 1 != range->till)) {
return std::nullopt;
}
const auto leaf = LeafPath{
.kind = LeafKind::BlockText,
.block = {
.container = range->container,
.index = range->from,
},
};
const auto owner = candidate.block(leaf.block);
if (!owner
|| ((owner->kind != BlockKind::Paragraph)
&& (owner->kind != BlockKind::Heading))
|| (candidate.textNodeOrdinal(leaf) < 0)) {
return std::nullopt;
}
return leaf;
};
if (const auto leaf = structuralTextBlockConversion()) {
const auto ordinal = candidate.textNodeOrdinal(*leaf);
const auto owner = candidate.block(leaf->block);
if (!owner || !candidate.setActiveTextByOrdinal(ordinal)) {
_lastLimitError = candidate._lastLimitError;
return false;
}
if (!candidate.insertBlockAfterActive(action, ActiveTextInsertContext{
.before = {},
.selected = owner->text.text,
.after = {},
})) {
_lastLimitError = candidate._lastLimitError;
return false;
}
if (destination && (candidate._activeTextOrdinal >= 0)) {
*destination = {
.action = BoundaryTarget::Action::Text,
.textOrdinal = candidate._activeTextOrdinal,
};
}
commitCheckedMutation(std::move(candidate));
return true;
}
auto target = BoundaryTarget();
switch (action.type) {
case InsertBlockType::Blockquote:
case InsertBlockType::Pullquote:
case InsertBlockType::Details:
if (selection.kind == PreparedEditSelectionKind::TableRows
|| selection.kind == PreparedEditSelectionKind::TableCells) {
return false;
}
if (candidate.unwrapMatchingStructuralWrapper(
selection,
action.type,
&target)) {
if (!commitValidatedCandidate(std::move(candidate))) {
return false;
}
if (destination) {
*destination = target;
}
return true;
}
if (selection.kind == PreparedEditSelectionKind::Blocks) {
if (!candidate.wrapStructuralBlockSelection(
selection,
action,
&target)) {
_lastLimitError = candidate._lastLimitError;
return false;
}
if (!commitValidatedCandidate(std::move(candidate))) {
return false;
}
if (destination) {
*destination = target;
}
return true;
}
break;
case InsertBlockType::OrderedList:
case InsertBlockType::BulletList:
if (selection.kind == PreparedEditSelectionKind::TableRows
|| selection.kind == PreparedEditSelectionKind::TableCells) {
return false;
}
if (candidate.unwrapMatchingListItemWrapper(
selection,
action.type,
&target)) {
if (!commitValidatedCandidate(std::move(candidate))) {
return false;
}
if (destination) {
*destination = target;
}
return true;
}
if (selection.kind == PreparedEditSelectionKind::Blocks) {
if (!candidate.wrapStructuralBlockSelection(
selection,
action,
&target)) {
_lastLimitError = candidate._lastLimitError;
return false;
}
if (!commitValidatedCandidate(std::move(candidate))) {
return false;
}
if (destination) {
*destination = target;
}
return true;
}
break;
default:
break;
}
if (!candidate.removeStructuralSelection(selection, true)) {
_lastLimitError = candidate._lastLimitError;
return false;
}
if (!candidate.insertBlockAfterActive(action, std::move(context))) {
_lastLimitError = candidate._lastLimitError;
return false;
}
if (destination && (candidate._activeTextOrdinal >= 0)) {
*destination = {
.action = BoundaryTarget::Action::Text,
.textOrdinal = candidate._activeTextOrdinal,
};
}
commitCheckedMutation(std::move(candidate));
return true;
}
bool State::toggleCodeBlockForStructuralSelection(
const Markdown::PreparedEditSelection &selection) {
_lastLimitError = std::nullopt;
if (selection.kind != PreparedEditSelectionKind::Blocks) {
return false;
}
const auto range = validateBlockRange(selection.blocks);
if (!range || (range->from + 1 != range->till)) {
return false;
}
const auto path = BlockPath{
.container = range->container,
.index = range->from,
};
const auto owner = block(path);
if (!owner) {
return false;
}
switch (owner->kind) {
case BlockKind::Paragraph:
return replaceStructuralSelectionWithBlock(selection, {
.type = InsertBlockType::Code,
});
case BlockKind::Code: {
return applyCheckedMutation(false, [selection](State &candidate) {
const auto range = candidate.validateBlockRange(selection.blocks);
if (!range || (range->from + 1 != range->till)) {
return CheckedMutationResult<bool>{ .result = false };
}
const auto path = BlockPath{
.container = range->container,
.index = range->from,
};
const auto owner = candidate.block(path);
if (!owner || owner->kind != BlockKind::Code) {
return CheckedMutationResult<bool>{ .result = false };
}
auto paragraph = MakeParagraphBlock();
paragraph.anchorId = owner->anchorId;
paragraph.text = owner->text;
auto blocks = std::vector<Block>();
blocks.push_back(std::move(paragraph));
auto insertAt = range->from;
if (!candidate.removeStructuralSelection(selection, true)) {
return CheckedMutationResult<bool>{ .result = false };
}
const auto normalized
= candidate.normalizeTextOnlyListItemForInsertion(
range->container);
if (normalized && (*normalized >= 0)) {
++insertAt;
}
if (!candidate.insertPreparedBlocksAtExplicitPosition(
std::move(blocks),
range->container,
&insertAt)) {
return CheckedMutationResult<bool>{ .result = false };
}
candidate.rebuild();
(void)candidate.destinationTargetForInsertedBlocks(
range->container,
insertAt,
1);
return CheckedMutationResult<bool>{
.apply = true,
.result = true,
};
});
}
default:
return false;
}
}
bool State::replaceStructuralSelectionWithPreparedBlocks(
const Markdown::PreparedEditSelection &selection,
std::vector<Block> blocks,
std::optional<ActiveTextInsertContext> context) {
_lastLimitError = std::nullopt;
auto candidate = State(
std::make_shared<RichPage>(*_richPage),
_mediaRuntime,
_limits);
candidate._activeTextOrdinal = _activeTextOrdinal;
candidate._lastLimitError = std::nullopt;
candidate._temporaryDownParagraph = _temporaryDownParagraph;
const auto blocksRange = (selection.kind == PreparedEditSelectionKind::Blocks)
? candidate.validateBlockRange(selection.blocks)
: std::nullopt;
if (!candidate.removeStructuralSelection(selection, true)) {
_lastLimitError = candidate._lastLimitError;
return false;
}
const auto inserted = blocksRange
? candidate.insertPreparedBlocksAtRemovedBlockRange(
std::move(blocks),
*blocksRange)
: candidate.insertPreparedBlocksAfterActive(
std::move(blocks),
std::move(context));
if (!inserted) {
_lastLimitError = candidate._lastLimitError;
return false;
}
commitCheckedMutation(std::move(candidate));
return true;
}
bool State::replaceStructuralSelectionWithClipboardListItems(
const Markdown::PreparedEditSelection &selection,
const ClipboardListItemsData &data,
std::optional<ActiveTextInsertContext> context) {
_lastLimitError = std::nullopt;
auto candidate = State(
std::make_shared<RichPage>(*_richPage),
_mediaRuntime,
_limits);
candidate._activeTextOrdinal = _activeTextOrdinal;
candidate._lastLimitError = std::nullopt;
candidate._temporaryDownParagraph = _temporaryDownParagraph;
if (!candidate.removeStructuralSelection(selection, true)) {
_lastLimitError = candidate._lastLimitError;
return false;
}
if (!candidate.pasteClipboardListItemsAfterActive(
data,
std::move(context))) {
_lastLimitError = candidate._lastLimitError;
return false;
}
commitCheckedMutation(std::move(candidate));
return true;
}
State::StructuralSelectionDropResult State::moveStructuralSelectionToDropTarget(
const PreparedEditSelection &selection,
const Markdown::PreparedEditDropTarget &target) {
struct BlockInsertionTarget {
BlockContainerPath container;
int insertIndex = -1;
};
struct ListInsertionTarget {
BlockPath block;
int insertIndex = -1;
};
const auto failure = StructuralSelectionDropResult{
.result = ApplyResult::Failed,
};
return applyCheckedMutation(failure, [selection, target](State &candidate) {
auto result = StructuralSelectionDropResult{
.result = ApplyResult::Failed,
};
const auto payload = candidate.structuredClipboardDataForSelection(
selection);
if (!payload) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
auto blockTarget = std::optional<BlockInsertionTarget>();
auto listTarget = std::optional<ListInsertionTarget>();
if (const auto block = std::get_if<Markdown::PreparedEditBlockDropTarget>(
&target)) {
const auto container = candidate.convertBlockContainerPath(
block->container);
if (!container
|| !candidate.blockContainer(*container)
|| block->insertIndex < 0) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
blockTarget = {
.container = *container,
.insertIndex = block->insertIndex,
};
} else if (const auto list
= std::get_if<Markdown::PreparedEditListItemDropTarget>(&target)) {
const auto blockPath = candidate.convertBlockPath(list->block);
const auto owner = blockPath ? candidate.block(*blockPath) : nullptr;
if (!blockPath
|| !owner
|| owner->kind != BlockKind::List
|| list->insertIndex < 0
|| list->insertIndex > int(owner->listItems.size())) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
listTarget = {
.block = *blockPath,
.insertIndex = list->insertIndex,
};
} else {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
switch (selection.kind) {
case PreparedEditSelectionKind::Blocks: {
const auto range = candidate.validateBlockRange(selection.blocks);
if (!range || !blockTarget) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
if (blockTarget->container == range->container) {
if (blockTarget->insertIndex >= range->from
&& blockTarget->insertIndex <= range->till) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
} else if (blockTarget->insertIndex > range->till) {
blockTarget->insertIndex -= (range->till - range->from);
}
}
for (auto i = range->till; i != range->from;) {
--i;
const auto removed = BlockPath{
.container = range->container,
.index = i,
};
if (!ShiftBlockContainerPathAfterRemovedBlock(
blockTarget->container,
removed)) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
}
} break;
case PreparedEditSelectionKind::ListItems: {
const auto range = candidate.validateListItemRange(
selection.listItems);
const auto owner = range ? candidate.block(range->block) : nullptr;
if (!range || !owner || owner->kind != BlockKind::List) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
const auto removesWholeList = (range->from == 0)
&& (range->till == int(owner->listItems.size()));
if (blockTarget) {
if (removesWholeList
&& blockTarget->container == range->block.container
&& blockTarget->insertIndex >= range->block.index
&& blockTarget->insertIndex <= range->block.index + 1) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
if (removesWholeList) {
if (blockTarget->container == range->block.container
&& blockTarget->insertIndex > range->block.index) {
--blockTarget->insertIndex;
}
if (!ShiftBlockContainerPathAfterRemovedBlock(
blockTarget->container,
range->block)) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
} else {
for (auto i = range->till; i != range->from;) {
--i;
if (!ShiftBlockContainerPathAfterRemovedListItem(
blockTarget->container,
range->block,
i)) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
}
}
} else if (listTarget) {
if (listTarget->block == range->block) {
if (listTarget->insertIndex >= range->from
&& listTarget->insertIndex <= range->till) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
} else if (listTarget->insertIndex > range->till) {
listTarget->insertIndex -= (range->till - range->from);
}
} else if (removesWholeList) {
if (!ShiftBlockPathAfterRemovedBlock(
listTarget->block,
range->block)) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
} else {
for (auto i = range->till; i != range->from;) {
--i;
if (!ShiftBlockPathAfterRemovedListItem(
listTarget->block,
range->block,
i)) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
}
}
} else {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
} break;
case PreparedEditSelectionKind::TableRows:
case PreparedEditSelectionKind::TableCells:
case PreparedEditSelectionKind::None:
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
if (!candidate.removeStructuralSelection(selection, true)) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
if (const auto blocks = std::get_if<ClipboardBlockData>(&*payload)) {
if (!blockTarget) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
auto inserted = blocks->blocks;
NormalizeInsertedOrderedListMetadata(&inserted);
candidate.normalizeInsertedBlockAnchors(inserted);
const auto count = int(inserted.size());
if (!candidate.insertPreparedBlocksAtExplicitPosition(
std::move(inserted),
blockTarget->container,
&blockTarget->insertIndex)) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
candidate.rebuild();
result.result = ApplyResult::Changed;
result.destination = candidate.destinationTargetForInsertedBlocks(
blockTarget->container,
blockTarget->insertIndex,
count);
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = true,
.result = result,
};
}
const auto items = std::get_if<ClipboardListItemsData>(&*payload);
if (!items) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
auto listBlock = Block();
listBlock.kind = BlockKind::List;
listBlock.listKind = items->listKind;
listBlock.orderedList = items->orderedList;
listBlock.listItems = items->items;
auto insertedBlocks = std::vector<Block>();
insertedBlocks.push_back(std::move(listBlock));
NormalizeInsertedOrderedListMetadata(&insertedBlocks);
candidate.normalizeInsertedBlockAnchors(insertedBlocks);
if (listTarget) {
const auto owner = candidate.block(listTarget->block);
if (!owner || owner->kind != BlockKind::List) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
if (ListBlockMatchesClipboardData(*owner, *items)) {
auto insertedItems = std::move(insertedBlocks.front().listItems);
const auto count = int(insertedItems.size());
if (!candidate.insertPreparedListItemsAtExplicitPosition(
std::move(insertedItems),
listTarget->block,
listTarget->insertIndex)) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
candidate.rebuild();
result.result = ApplyResult::Changed;
result.destination = candidate.destinationTargetForInsertedListItems(
listTarget->block,
listTarget->insertIndex,
count);
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = true,
.result = result,
};
}
auto container = listTarget->block.container;
auto insertAt = listTarget->block.index;
auto trailingBlocks = std::vector<Block>();
const auto splitLeadingStart = (owner->listKind == ListKind::Ordered
&& listTarget->insertIndex > 0
&& listTarget->insertIndex < int(owner->listItems.size()))
? EffectiveOrderedItemValue(*owner, 0)
: std::optional<int>();
const auto splitTrailingStart = (owner->listKind == ListKind::Ordered
&& listTarget->insertIndex > 0
&& listTarget->insertIndex < int(owner->listItems.size()))
? EffectiveOrderedItemValue(*owner, listTarget->insertIndex)
: std::optional<int>();
if (listTarget->insertIndex > 0) {
insertAt = listTarget->block.index + 1;
if (listTarget->insertIndex < int(owner->listItems.size())) {
auto trailing = Block();
trailing.kind = BlockKind::List;
trailing.listKind = owner->listKind;
trailing.orderedList = owner->orderedList;
if (splitTrailingStart.has_value()) {
trailing.orderedList.start = splitTrailingStart;
}
trailing.listItems = std::vector<ListItem>(
std::make_move_iterator(
owner->listItems.begin() + listTarget->insertIndex),
std::make_move_iterator(owner->listItems.end()));
owner->listItems.erase(
owner->listItems.begin() + listTarget->insertIndex,
owner->listItems.end());
if (splitLeadingStart.has_value()) {
owner->orderedList.start = splitLeadingStart;
}
trailingBlocks.push_back(std::move(trailing));
}
}
NormalizeInsertedOrderedListMetadata(&trailingBlocks);
if (!candidate.insertPreparedBlocksAtExplicitPosition(
std::move(insertedBlocks),
container,
&insertAt)) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
auto trailingInsertAt = insertAt + 1;
if (!trailingBlocks.empty()
&& !candidate.insertPreparedBlocksAtExplicitPosition(
std::move(trailingBlocks),
container,
&trailingInsertAt)) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
candidate.rebuild();
result.result = ApplyResult::Changed;
result.destination = candidate.destinationTargetForInsertedBlocks(
container,
insertAt,
1);
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = true,
.result = result,
};
}
if (!blockTarget
|| !candidate.insertPreparedBlocksAtExplicitPosition(
std::move(insertedBlocks),
blockTarget->container,
&blockTarget->insertIndex)) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
candidate.rebuild();
result.result = ApplyResult::Changed;
result.destination = candidate.destinationTargetForInsertedBlocks(
blockTarget->container,
blockTarget->insertIndex,
1);
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = true,
.result = result,
};
});
}
State::TextSelectionDropResult State::moveTextSelectionToDropTarget(
const std::vector<TextNodeSpan> &source,
const Markdown::PreparedEditDropTarget &target) {
const auto failure = TextSelectionDropResult{
.result = ApplyResult::Failed,
};
if (source.empty()) {
return failure;
}
return applyCheckedMutation(failure, [source, target](State &candidate) {
struct SourceRewrite {
LeafPath leaf;
TextWithEntities text;
};
auto result = TextSelectionDropResult{
.result = ApplyResult::Failed,
};
auto moved = TextWithEntities();
auto sourceRewrites = std::vector<SourceRewrite>();
sourceRewrites.reserve(source.size());
for (const auto &span : source) {
const auto current = candidate.richText(span.leaf);
if (!current) {
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
auto sourceBefore = TextWithEntities();
auto selected = TextWithEntities();
auto sourceAfter = TextWithEntities();
if (!SplitTextSpan(
current->text,
span.from,
span.till,
&sourceBefore,
&selected,
&sourceAfter)) {
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
moved.append(std::move(selected));
sourceRewrites.push_back(SourceRewrite{
.leaf = span.leaf,
.text = JoinText(
std::move(sourceBefore),
TextWithEntities(),
std::move(sourceAfter)),
});
}
const auto movedLength = int(moved.text.size());
const auto applySourceRewrites = [&](std::vector<SourceRewrite> rewrites) {
for (auto &rewrite : rewrites) {
const auto current = candidate.richText(rewrite.leaf);
if (!current) {
return false;
}
current->text = std::move(rewrite.text);
}
return true;
};
const auto finishAtLeaf = [&](
const LeafPath &leaf,
int selectionFrom,
int selectionTo) {
candidate.rebuild();
const auto ordinal = candidate.textOrdinalForLeafPath(leaf);
if (ordinal >= 0) {
(void)candidate.setActiveTextByOrdinal(ordinal);
} else {
candidate.ensureActiveTextOrdinal();
}
result.result = ApplyResult::Changed;
result.destinationLeaf = leaf;
result.selectionFrom = selectionFrom;
result.selectionTo = selectionTo;
return CheckedMutationResult<TextSelectionDropResult>{
.apply = true,
.result = result,
};
};
if (const auto text = std::get_if<Markdown::PreparedEditTextDropTarget>(
&target)) {
const auto destinationLeaf = candidate.convertLeafPath(text->leaf);
const auto destination = destinationLeaf
? candidate.richText(*destinationLeaf)
: nullptr;
if (!destination
|| (text->leaf.kind
== Markdown::PreparedEditLeafKind::MathFormula)
|| !RangeInsideText(destination->text.text, text->offset, 0)) {
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
auto insertAt = text->offset;
auto destinationRewrite = -1;
for (auto i = 0, count = int(source.size()); i != count; ++i) {
const auto &span = source[i];
if (!(span.leaf == *destinationLeaf)) {
continue;
}
if (text->offset >= span.from && text->offset <= span.till) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
if (span.from < insertAt) {
insertAt -= std::min(span.till, insertAt) - span.from;
}
destinationRewrite = i;
}
const auto destinationText = (destinationRewrite >= 0)
? sourceRewrites[destinationRewrite].text
: destination->text;
if (!RangeInsideText(destinationText.text, insertAt, 0)) {
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
auto destinationBefore = Ui::Text::Mid(destinationText, 0, insertAt);
auto destinationAfter = Ui::Text::Mid(destinationText, insertAt);
auto updated = JoinText(
std::move(destinationBefore),
std::move(moved),
std::move(destinationAfter));
if (destinationRewrite >= 0) {
sourceRewrites[destinationRewrite].text = std::move(updated);
} else {
destination->text = std::move(updated);
}
if (!applySourceRewrites(std::move(sourceRewrites))) {
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
return finishAtLeaf(
*destinationLeaf,
insertAt,
insertAt + movedLength);
}
const auto block = std::get_if<Markdown::PreparedEditBlockDropTarget>(
&target);
const auto container = block
? candidate.convertBlockContainerPath(block->container)
: std::nullopt;
const auto destination = container
? candidate.blockContainer(*container)
: nullptr;
if (!block
|| !destination
|| block->insertIndex < 0) {
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
auto paragraph = MakeParagraphBlock();
paragraph.text.text = std::move(moved);
auto blocks = std::vector<Block>();
blocks.push_back(std::move(paragraph));
if (!applySourceRewrites(std::move(sourceRewrites))) {
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
auto insertIndex = block->insertIndex;
if (!candidate.insertPreparedBlocksAtExplicitPosition(
std::move(blocks),
*container,
&insertIndex)) {
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
return finishAtLeaf(
LeafPath{
.kind = LeafKind::BlockText,
.block = BlockPath{
.container = *container,
.index = insertIndex,
},
},
0,
movedLength);
});
}
State::TextSelectionDropResult State::moveTextSelectionToDropTarget(
const TextNodeSpan &source,
const Markdown::PreparedEditDropTarget &target) {
return moveTextSelectionToDropTarget(
std::vector<TextNodeSpan>{ source },
target);
}
void State::insertHeading1AfterActive() {
(void)insertBlockAfterActive({
.type = InsertBlockType::Heading,
.headingLevel = 1,
});
}
void State::insertBlockquoteAfterActive() {
(void)insertBlockAfterActive({
.type = InsertBlockType::Blockquote,
});
}
bool State::insertBlocksAfterActiveWithContextUnchecked(
std::vector<Block> &blocks,
const ActiveTextInsertContext &context) {
const auto descriptor = textNode(_activeTextOrdinal);
if (!descriptor || descriptor->leaf.kind == LeafKind::TableCellText) {
return false;
}
const auto target = resolveActiveTextInsertTarget();
if (!target) {
return false;
}
auto container = target->anchor.container;
auto insertAt = target->anchor.blockIndex + 1;
auto removeSource = false;
if (target->leaf.kind == LeafKind::BlockText) {
if (const auto owner = block(target->leaf.block);
owner
&& context.before.text.isEmpty()
&& ((owner->kind == BlockKind::Paragraph)
|| (owner->kind == BlockKind::Heading))) {
removeSource = true;
container = target->leaf.block.container;
insertAt = target->leaf.block.index;
}
}
auto *destination = blockContainer(container);
if (!destination) {
return false;
}
if (removeSource) {
if (insertAt < 0 || insertAt >= int(destination->size())) {
return false;
}
} else {
const auto current = richText(target->leaf);
if (!current) {
return false;
}
current->text = context.before;
}
(void)seedInsertedBlocks(blocks, context.selected);
if (removeSource) {
destination->erase(destination->begin() + insertAt);
}
const auto count = int(blocks.size());
if (insertAt < 0 || insertAt > int(destination->size())) {
return false;
}
destination->insert(
destination->begin() + insertAt,
std::make_move_iterator(blocks.begin()),
std::make_move_iterator(blocks.end()));
if (!appendInsertedTrailingText(
container,
insertAt,
count,
context.after)) {
return false;
}
rebuild();
focusInsertedBlocks(container, insertAt, count);
return true;
}
State::ActiveTextBlockActionResult State::applyActiveTextBlockAction(
InsertAction action,
ActiveTextInsertContext context) {
return applyCheckedMutation(ActiveTextBlockActionResult{
.result = ApplyResult::Failed,
}, [action, context = std::move(context)](State &candidate) mutable {
ActiveTextSelectionTarget target;
ActiveTextBlockActionResult result{
.result = ApplyResult::Failed,
};
const auto changed = [&] {
result = {
.result = ApplyResult::Changed,
.destinationLeaf = target.leaf,
.selectionFrom = target.selectionFrom,
.selectionTo = target.selectionTo,
};
return CheckedMutationResult<ActiveTextBlockActionResult>{
.apply = true,
.result = result,
};
};
if (action.type == InsertBlockType::Blockquote) {
if (candidate.activeNonPullquoteQuote()) {
if (candidate.unwrapActiveBlockquoteUnchecked(context, &target)) {
return changed();
}
return CheckedMutationResult<ActiveTextBlockActionResult>{
.result = result,
};
}
}
if (action.type == InsertBlockType::Code
&& candidate.unwrapActiveCodeBlockUnchecked(context, &target)) {
return changed();
}
auto blocks = std::vector<Block>();
blocks.push_back(candidate.makeBlock(action));
const auto applied = candidate.insertBlocksAfterActiveUnchecked(
std::move(blocks),
context);
if (!applied) {
return CheckedMutationResult<ActiveTextBlockActionResult>{
.result = result,
};
}
const auto descriptor = candidate.textNode(candidate._activeTextOrdinal);
if (!descriptor) {
return CheckedMutationResult<ActiveTextBlockActionResult>{
.result = result,
};
}
return CheckedMutationResult<ActiveTextBlockActionResult>{
.apply = true,
.result = {
.result = ApplyResult::Changed,
.destinationLeaf = descriptor->leaf,
.selectionFrom = 0,
.selectionTo = int(context.selected.text.size()),
},
};
});
}
State::ActiveTextBlockActionResult State::replaceActiveTextSelectionWithText(
TextWithEntities text,
ActiveTextInsertContext context) {
return applyCheckedMutation(ActiveTextBlockActionResult{
.result = ApplyResult::Failed,
}, [text = std::move(text), context = std::move(context)](
State &candidate) mutable {
const auto failed = [&] {
return CheckedMutationResult<ActiveTextBlockActionResult>{
.result = ActiveTextBlockActionResult{
.result = ApplyResult::Failed,
},
};
};
if (text.text.isEmpty()) {
return failed();
}
const auto descriptor = candidate.textNode(
candidate._activeTextOrdinal);
if (!descriptor || (descriptor->leaf.kind == LeafKind::MathFormula)) {
return failed();
}
const auto selectionFrom = int(context.before.text.size());
const auto selectionTo = selectionFrom + int(text.text.size());
const auto applied = candidate.applyActiveTextUnchecked(JoinText(
std::move(context.before),
std::move(text),
std::move(context.after)));
if (applied == ApplyResult::Failed) {
return failed();
}
const auto updated = candidate.textNode(candidate._activeTextOrdinal);
return CheckedMutationResult<ActiveTextBlockActionResult>{
.apply = true,
.result = {
.result = ApplyResult::Changed,
.destinationLeaf = (updated
? updated->leaf
: descriptor->leaf),
.selectionFrom = selectionFrom,
.selectionTo = selectionTo,
},
};
});
}
bool State::insertBlockAfterActive(
InsertAction action,
std::optional<ActiveTextInsertContext> context) {
return applyCheckedMutation(false, [action, context = std::move(context)](
State &candidate) mutable {
auto blocks = std::vector<Block>();
blocks.push_back(candidate.makeBlock(action));
if (action.type == InsertBlockType::Divider) {
// A divider has no editable content, so insert a paragraph
// together with it: the selected text piece (if any) seeds into
// the paragraph and focus lands there, keeping an editable spot
// below the divider while the block above stays editable too.
blocks.push_back(MakeParagraphBlock());
if (context) {
// Treat it as a paragraph split with a divider in between:
// everything from the cursor on moves into the paragraph
// below the divider instead of a separate trailing one.
context->selected = JoinText(
std::move(context->selected),
std::move(context->after),
{});
context->after = {};
}
}
const auto applied = candidate.insertBlocksAfterActiveUnchecked(
std::move(blocks),
std::move(context));
return CheckedMutationResult<bool>{
.apply = applied,
.result = applied,
};
});
}
bool State::insertPreparedBlockAfterActive(Block block) {
auto blocks = std::vector<Block>();
blocks.push_back(std::move(block));
return insertPreparedBlocksAfterActive(std::move(blocks));
}
bool State::insertPreparedBlocksAfterActive(
std::vector<Block> blocks,
std::optional<ActiveTextInsertContext> context) {
return applyCheckedMutation(false, [
blocks = std::move(blocks),
context = std::move(context)](State &candidate) mutable {
const auto applied = candidate.insertBlocksAfterActiveUnchecked(
std::move(blocks),
std::move(context));
return CheckedMutationResult<bool>{
.apply = applied,
.result = applied,
};
});
}
State::DisplayMathEditResult State::editActiveDisplayMath(
QString source,
bool separateLine) {
auto failure = DisplayMathEditResult{
.result = ApplyResult::Failed,
};
return applyCheckedMutation(failure, [
source = std::move(source),
separateLine](State &candidate) mutable {
const auto descriptor = candidate.textNode(candidate._activeTextOrdinal);
if (!descriptor || descriptor->leaf.kind != LeafKind::MathFormula) {
return CheckedMutationResult<DisplayMathEditResult>{
.result = {
.result = ApplyResult::Failed,
},
};
}
const auto leaf = descriptor->leaf;
auto *blocks = candidate.blockContainer(leaf.block.container);
if (!blocks
|| leaf.block.index < 0
|| leaf.block.index >= int(blocks->size())) {
return CheckedMutationResult<DisplayMathEditResult>{
.result = {
.result = ApplyResult::Failed,
},
};
}
auto &math = (*blocks)[leaf.block.index];
if (math.kind != BlockKind::Math) {
return CheckedMutationResult<DisplayMathEditResult>{
.result = {
.result = ApplyResult::Failed,
},
};
}
if (separateLine) {
if (math.formula == source) {
return CheckedMutationResult<DisplayMathEditResult>{
.result = {
.result = ApplyResult::Unchanged,
},
};
}
math.formula = std::move(source);
candidate.rebuild();
if (!candidate.activateRebuiltLeaf(leaf)) {
return CheckedMutationResult<DisplayMathEditResult>{
.result = {
.result = ApplyResult::Failed,
},
};
}
return CheckedMutationResult<DisplayMathEditResult>{
.apply = true,
.result = {
.result = ApplyResult::Changed,
},
};
}
auto inlineMath = FormulaSourceToRichText(std::move(source));
const auto mathIndex = leaf.block.index;
const auto previousIndex = mathIndex - 1;
const auto nextIndex = mathIndex + 1;
const auto paragraphAt = [&](int index) -> Block* {
return (index >= 0
&& index < int(blocks->size())
&& (*blocks)[index].kind == BlockKind::Paragraph)
? &(*blocks)[index]
: nullptr;
};
auto *previous = paragraphAt(previousIndex);
auto *next = paragraphAt(nextIndex);
const auto previousHasText = previous
&& RichTextHasVisibleText(previous->text);
const auto nextHasText = next
&& RichTextHasVisibleText(next->text);
enum class Target {
Previous,
Next,
Replace,
};
auto target = Target::Replace;
auto removePrevious = false;
auto removeNext = false;
if (previousHasText) {
target = Target::Previous;
removeNext = (next != nullptr);
} else if (nextHasText) {
target = Target::Next;
removePrevious = (previous != nullptr);
} else if (previous) {
target = Target::Previous;
removeNext = (next != nullptr);
} else if (next) {
target = Target::Next;
}
auto inlineLeaf = LeafPath{
.kind = LeafKind::BlockText,
.block = {
.container = leaf.block.container,
.index = mathIndex,
},
};
auto selectionFrom = 0;
auto selectionTo = 0;
switch (target) {
case Target::Previous: {
auto updated = std::move(previous->text.text);
if (previousHasText) {
updated.append(' ');
}
selectionFrom = int(updated.text.size());
updated.append(std::move(inlineMath));
selectionTo = int(updated.text.size());
if (next) {
if (nextHasText) {
updated.append(' ');
}
updated.append(next->text.text);
}
previous->text.text = std::move(updated);
if (next) {
MergeRichTextAnchors(&previous->text, std::move(next->text));
}
if (removeNext) {
blocks->erase(blocks->begin() + nextIndex);
}
blocks->erase(blocks->begin() + mathIndex);
inlineLeaf.block.index = previousIndex;
} break;
case Target::Next: {
auto updated = TextWithEntities();
auto nextText = std::move(next->text.text);
updated.append(std::move(inlineMath));
selectionFrom = 0;
selectionTo = updated.text.size();
if (nextHasText) {
updated.append(' ');
}
updated.append(std::move(nextText));
next->text.text = std::move(updated);
if (previous && removePrevious) {
MergeRichTextAnchors(&next->text, std::move(previous->text));
}
blocks->erase(blocks->begin() + mathIndex);
inlineLeaf.block.index = mathIndex;
if (removePrevious) {
blocks->erase(blocks->begin() + previousIndex);
inlineLeaf.block.index = previousIndex;
}
} break;
case Target::Replace: {
auto paragraph = MakeParagraphBlock();
paragraph.text.text = std::move(inlineMath);
selectionTo = paragraph.text.text.text.size();
(*blocks)[mathIndex] = std::move(paragraph);
} break;
}
candidate.rebuild();
if (!candidate.activateRebuiltLeaf(inlineLeaf)) {
return CheckedMutationResult<DisplayMathEditResult>{
.result = {
.result = ApplyResult::Failed,
},
};
}
return CheckedMutationResult<DisplayMathEditResult>{
.apply = true,
.result = {
.result = ApplyResult::Changed,
.inlineLeaf = inlineLeaf,
.selectionFrom = selectionFrom,
.selectionTo = selectionTo,
},
};
});
}
bool State::insertBlocksAfterActiveUnchecked(
std::vector<Block> blocks,
std::optional<ActiveTextInsertContext> context) {
if (blocks.empty()) {
return false;
}
clearTemporaryDownParagraph();
NormalizeInsertedOrderedListMetadata(&blocks);
normalizeInsertedBlockAnchors(blocks);
if (context) {
if (insertBlocksAfterActiveWithContextUnchecked(blocks, *context)) {
return true;
}
if (applyActiveTextUnchecked(JoinText(
context->before,
context->selected,
context->after)) == ApplyResult::Failed) {
return false;
}
}
const auto descriptor = textNode(_activeTextOrdinal);
if (descriptor && shouldReplaceActiveTextOnlyBlock(*descriptor, blocks)) {
const auto path = descriptor->removalTarget.block;
const auto container = blockContainer(path.container);
if (container
&& path.index >= 0
&& path.index < int(container->size())) {
const auto insertAt = path.index;
const auto count = int(blocks.size());
container->erase(container->begin() + insertAt);
container->insert(
container->begin() + insertAt,
std::make_move_iterator(blocks.begin()),
std::make_move_iterator(blocks.end()));
rebuild();
focusInsertedBlocks(path.container, insertAt, count);
return true;
}
}
auto anchor = resolveActiveInsertionTarget();
if (const auto normalized = normalizeTextOnlyListItemForInsertion(
anchor.container)) {
anchor.blockIndex = *normalized;
} else if (const auto normalized = normalizeTextOnlyQuoteForInsertion(
anchor.container)) {
anchor.blockIndex = *normalized;
}
auto *container = blockContainer(anchor.container);
if (!container) {
anchor = InsertionAnchor{
.container = BlockContainerPath(),
.blockIndex = int(_richPage->blocks.size()) - 1,
};
container = &_richPage->blocks;
}
const auto insertAt = std::clamp(
anchor.blockIndex + 1,
0,
int(container->size()));
const auto count = int(blocks.size());
container->insert(
container->begin() + insertAt,
std::make_move_iterator(blocks.begin()),
std::make_move_iterator(blocks.end()));
rebuild();
focusInsertedBlocks(anchor.container, insertAt, count);
return true;
}
bool State::insertPreparedBlocksAtExplicitPosition(
std::vector<Block> blocks,
const BlockContainerPath &container,
int *insertAt) {
if (!normalizeTextOnlyContainerForInsertion(container, insertAt)) {
return false;
}
auto *destination = blockContainer(container);
if (!destination || *insertAt > int(destination->size())) {
return false;
}
NormalizeInsertedOrderedListMetadata(&blocks);
destination->insert(
destination->begin() + *insertAt,
std::make_move_iterator(blocks.begin()),
std::make_move_iterator(blocks.end()));
return true;
}
bool State::insertPreparedBlocksAtRemovedBlockRange(
std::vector<Block> blocks,
const StructuralBlockRange &range) {
if (blocks.empty()) {
return false;
}
return applyCheckedMutation(false, [
blocks = std::move(blocks),
range](State &candidate) mutable {
candidate.normalizeInsertedBlockAnchors(blocks);
auto insertAt = range.from;
const auto count = int(blocks.size());
if (!candidate.insertPreparedBlocksAtExplicitPosition(
std::move(blocks),
range.container,
&insertAt)) {
return CheckedMutationResult<bool>{ .result = false };
}
candidate.rebuild();
candidate.focusInsertedBlocks(range.container, insertAt, count);
return CheckedMutationResult<bool>{ .apply = true, .result = true };
});
}
bool State::insertPreparedBlocksAtDropTarget(
std::vector<Block> blocks,
const Markdown::PreparedEditBlockDropTarget &target) {
if (blocks.empty() || target.insertIndex < 0) {
return false;
}
return applyCheckedMutation(false, [
blocks = std::move(blocks),
target](State &candidate) mutable {
const auto container = candidate.convertBlockContainerPath(
target.container);
if (!container || !candidate.blockContainer(*container)) {
return CheckedMutationResult<bool>{ .result = false };
}
candidate.normalizeInsertedBlockAnchors(blocks);
auto insertAt = target.insertIndex;
if (!candidate.insertPreparedBlocksAtExplicitPosition(
std::move(blocks),
*container,
&insertAt)) {
return CheckedMutationResult<bool>{ .result = false };
}
candidate.rebuild();
return CheckedMutationResult<bool>{ .apply = true, .result = true };
});
}
bool State::insertPreparedListItemsAtExplicitPosition(
std::vector<ListItem> items,
const BlockPath &path,
int insertAt) {
auto *owner = block(path);
if (!owner
|| owner->kind != BlockKind::List
|| insertAt < 0
|| insertAt > int(owner->listItems.size())) {
return false;
}
owner->listItems.insert(
owner->listItems.begin() + insertAt,
std::make_move_iterator(items.begin()),
std::make_move_iterator(items.end()));
return true;
}
std::vector<Block> *State::blockContainer(const BlockContainerPath &path) {
auto *blocks = &_richPage->blocks;
for (const auto &step : path.steps) {
if (step.blockIndex < 0 || step.blockIndex >= int(blocks->size())) {
return nullptr;
}
auto &parent = (*blocks)[step.blockIndex];
if (step.kind == BlockContainerKind::BlockChildren) {
blocks = &parent.blocks;
} else if (step.kind == BlockContainerKind::ListItemChildren) {
if (step.listItemIndex < 0
|| step.listItemIndex >= int(parent.listItems.size())) {
return nullptr;
}
blocks = &parent.listItems[step.listItemIndex].blocks;
} else {
return nullptr;
}
}
return blocks;
}
const std::vector<Block> *State::blockContainer(
const BlockContainerPath &path) const {
const auto *blocks = &_richPage->blocks;
for (const auto &step : path.steps) {
if (step.blockIndex < 0 || step.blockIndex >= int(blocks->size())) {
return nullptr;
}
const auto &parent = (*blocks)[step.blockIndex];
if (step.kind == BlockContainerKind::BlockChildren) {
blocks = &parent.blocks;
} else if (step.kind == BlockContainerKind::ListItemChildren) {
if (step.listItemIndex < 0
|| step.listItemIndex >= int(parent.listItems.size())) {
return nullptr;
}
blocks = &parent.listItems[step.listItemIndex].blocks;
} else {
return nullptr;
}
}
return blocks;
}
Block *State::block(const BlockPath &path) {
const auto blocks = blockContainer(path.container);
if (!blocks || path.index < 0 || path.index >= int(blocks->size())) {
return nullptr;
}
return &(*blocks)[path.index];
}
const Block *State::block(const BlockPath &path) const {
const auto blocks = blockContainer(path.container);
if (!blocks || path.index < 0 || path.index >= int(blocks->size())) {
return nullptr;
}
return &(*blocks)[path.index];
}
ListItem *State::listItem(const BlockPath &blockPath, int itemIndex) {
const auto list = block(blockPath);
if (!list
|| itemIndex < 0
|| itemIndex >= int(list->listItems.size())) {
return nullptr;
}
return &list->listItems[itemIndex];
}
const ListItem *State::listItem(
const BlockPath &blockPath,
int itemIndex) const {
const auto list = block(blockPath);
if (!list
|| itemIndex < 0
|| itemIndex >= int(list->listItems.size())) {
return nullptr;
}
return &list->listItems[itemIndex];
}
TableCell *State::tableCell(
const BlockPath &blockPath,
int rowIndex,
int cellIndex) {
const auto table = block(blockPath);
if (!table
|| rowIndex < 0
|| rowIndex >= int(table->tableRows.size())) {
return nullptr;
}
auto &row = table->tableRows[rowIndex];
if (cellIndex < 0 || cellIndex >= int(row.cells.size())) {
return nullptr;
}
return &row.cells[cellIndex];
}
const TableCell *State::tableCell(
const BlockPath &blockPath,
int rowIndex,
int cellIndex) const {
const auto table = block(blockPath);
if (!table
|| rowIndex < 0
|| rowIndex >= int(table->tableRows.size())) {
return nullptr;
}
const auto &row = table->tableRows[rowIndex];
if (cellIndex < 0 || cellIndex >= int(row.cells.size())) {
return nullptr;
}
return &row.cells[cellIndex];
}
RichText *State::richText(const LeafPath &path) {
switch (path.kind) {
case LeafKind::BlockText:
if (const auto owner = block(path.block)) {
return &owner->text;
}
return nullptr;
case LeafKind::BlockCaption:
if (const auto owner = block(path.block)) {
return &owner->caption;
}
return nullptr;
case LeafKind::ListItemText:
if (const auto item = listItem(path.block, path.listItemIndex)) {
return &item->text;
}
return nullptr;
case LeafKind::TableCellText:
if (const auto cell = tableCell(
path.block,
path.tableRowIndex,
path.tableCellIndex)) {
return &cell->text;
}
return nullptr;
case LeafKind::MathFormula:
return nullptr;
}
return nullptr;
}
const RichText *State::richText(const LeafPath &path) const {
switch (path.kind) {
case LeafKind::BlockText:
if (const auto owner = block(path.block)) {
return &owner->text;
}
return nullptr;
case LeafKind::BlockCaption:
if (const auto owner = block(path.block)) {
return &owner->caption;
}
return nullptr;
case LeafKind::ListItemText:
if (const auto item = listItem(path.block, path.listItemIndex)) {
return &item->text;
}
return nullptr;
case LeafKind::TableCellText:
if (const auto cell = tableCell(
path.block,
path.tableRowIndex,
path.tableCellIndex)) {
return &cell->text;
}
return nullptr;
case LeafKind::MathFormula:
return nullptr;
}
return nullptr;
}
QString *State::rawText(const LeafPath &path) {
if (path.kind != LeafKind::MathFormula) {
return nullptr;
}
const auto owner = block(path.block);
return owner ? &owner->formula : nullptr;
}
const QString *State::rawText(const LeafPath &path) const {
if (path.kind != LeafKind::MathFormula) {
return nullptr;
}
const auto owner = block(path.block);
return owner ? &owner->formula : nullptr;
}
const TextNodeDescriptor *State::textNode(int ordinal) const {
return (ordinal >= 0 && ordinal < textNodeCount())
? &_textNodes[ordinal]
: nullptr;
}
int State::textNodeOrdinal(const LeafPath &path) const {
for (auto i = 0, count = textNodeCount(); i != count; ++i) {
if (_textNodes[i].leaf == path) {
return i;
}
}
return -1;
}
bool State::leafMutationKeepsTextNodes(
const TextNodeDescriptor &descriptor) const {
switch (descriptor.leaf.kind) {
case LeafKind::BlockText:
if (const auto owner = block(descriptor.leaf.block)) {
switch (owner->kind) {
case BlockKind::Heading:
case BlockKind::Paragraph:
case BlockKind::Footer:
case BlockKind::Code:
case BlockKind::Details:
return true;
case BlockKind::Quote:
return owner->blocks.empty();
case BlockKind::Table:
return !owner->text.text.text.isEmpty();
default:
return false;
}
}
return false;
case LeafKind::BlockCaption:
case LeafKind::TableCellText:
case LeafKind::MathFormula:
return true;
case LeafKind::ListItemText:
if (const auto item = listItem(
descriptor.leaf.block,
descriptor.leaf.listItemIndex)) {
return !RichTextIsEmpty(item->text) || item->blocks.empty();
}
return false;
}
return false;
}
bool State::updatePreparedActiveLeaf(
const TextNodeDescriptor &descriptor) {
const auto source = convertPreparedLeafSource(descriptor);
if (!source) {
return false;
}
return (Markdown::UpdatePreparedNativeInstantViewLeaf(
&_prepared,
*_richPage,
*source,
tableRenderLimits()) == NativeInstantViewLeafUpdateResult::Updated);
}
void State::rebuild() {
_lastPreparedMutationKind = PreparedMutationKind::FullRebuild;
rebuildTextNodes();
ensureEditableNodes();
ensureActiveTextOrdinal();
clearTemporaryDownParagraphIfInvalid();
rebuildPrepared();
}
void State::rebuildPrepared() {
_lastPreparedMutationKind = PreparedMutationKind::FullRebuild;
_prepared = Markdown::TryPrepareNativeInstantView({
.richPage = _richPage,
.mediaRuntime = _mediaRuntime,
.tableRenderLimits = tableRenderLimits(),
.editMode = true,
}).content;
}
void State::rebuildTextNodes() {
_textNodes.clear();
_textNodes.reserve(_richPage->blocks.size() * 3);
rebuildTextNodes(_richPage->blocks, BlockContainerPath());
}
void State::rebuildTextNodes(
const std::vector<Block> &blocks,
const BlockContainerPath &container) {
for (auto i = 0, count = int(blocks.size()); i != count; ++i) {
const auto path = BlockPath{
.container = container,
.index = i,
};
const auto &block = blocks[i];
switch (block.kind) {
case BlockKind::Heading:
case BlockKind::Paragraph:
case BlockKind::Footer:
case BlockKind::Code:
appendBlockTextNode(path, LeafKind::BlockText);
break;
case BlockKind::Quote:
if (block.blocks.empty()) {
appendBlockTextNode(
path,
LeafKind::BlockText,
FieldMode::Rich,
!block.pullquote
? std::make_optional(InsertionAnchor{
.container = BlockChildrenContainer(path),
.blockIndex = -1,
})
: std::nullopt);
}
rebuildTextNodes(block.blocks, BlockChildrenContainer(path));
appendBlockTextNode(path, LeafKind::BlockCaption);
break;
case BlockKind::List:
for (auto j = 0, itemCount = int(block.listItems.size());
j != itemCount;
++j) {
const auto &item = block.listItems[j];
if (!RichTextIsEmpty(item.text) || item.blocks.empty()) {
appendListItemTextNode(path, j);
}
if (!item.blocks.empty()) {
rebuildTextNodes(
item.blocks,
ListItemChildrenContainer(path, j));
}
}
break;
case BlockKind::Photo:
case BlockKind::Video:
case BlockKind::Audio:
case BlockKind::Map:
appendBlockTextNode(path, LeafKind::BlockCaption);
break;
case BlockKind::Math:
appendBlockTextNode(path, LeafKind::MathFormula, FieldMode::Raw);
break;
case BlockKind::Table:
appendBlockTextNode(path, LeafKind::BlockText);
for (auto j = 0, rowCount = int(block.tableRows.size());
j != rowCount;
++j) {
const auto &row = block.tableRows[j];
for (auto k = 0, cellCount = int(row.cells.size());
k != cellCount;
++k) {
appendTableCellTextNode(path, j, k);
}
}
break;
case BlockKind::Details:
appendBlockTextNode(
path,
LeafKind::BlockText,
FieldMode::Rich,
InsertionAnchor{
.container = BlockChildrenContainer(path),
.blockIndex = -1,
});
rebuildTextNodes(block.blocks, BlockChildrenContainer(path));
break;
default:
rebuildTextNodes(block.blocks, BlockChildrenContainer(path));
break;
}
}
}
void State::appendBlockTextNode(
const BlockPath &path,
LeafKind kind,
FieldMode mode,
std::optional<InsertionAnchor> insertionAnchor) {
_textNodes.push_back({
.leaf = {
.kind = kind,
.block = path,
},
.insertionAnchor = insertionAnchor.value_or(InsertionAnchor{
.container = path.container,
.blockIndex = path.index,
}),
.removalTarget = {
.kind = RemovalKind::Block,
.block = path,
},
.mode = mode,
});
}
void State::appendListItemTextNode(const BlockPath &path, int itemIndex) {
_textNodes.push_back({
.leaf = {
.kind = LeafKind::ListItemText,
.block = path,
.listItemIndex = itemIndex,
},
.insertionAnchor = {
.container = ListItemChildrenContainer(path, itemIndex),
.blockIndex = -1,
},
.removalTarget = {
.kind = RemovalKind::ListItem,
.block = path,
.listItemIndex = itemIndex,
},
.mode = FieldMode::Rich,
});
}
void State::appendTableCellTextNode(
const BlockPath &path,
int rowIndex,
int cellIndex) {
_textNodes.push_back({
.leaf = {
.kind = LeafKind::TableCellText,
.block = path,
.tableRowIndex = rowIndex,
.tableCellIndex = cellIndex,
},
.insertionAnchor = {
.container = path.container,
.blockIndex = path.index,
},
.removalTarget = {
.kind = RemovalKind::TableCell,
.block = path,
.tableRowIndex = rowIndex,
.tableCellIndex = cellIndex,
},
.mode = FieldMode::Rich,
});
}
void State::ensureActiveTextOrdinal() {
if (_textNodes.empty()) {
_activeTextOrdinal = -1;
} else if (_activeTextOrdinal < 0 || _activeTextOrdinal >= textNodeCount()) {
_activeTextOrdinal = 0;
}
}
void State::ensureEditableNodes() {
if (!_textNodes.empty()) {
return;
}
_richPage->blocks.push_back(MakeParagraphBlock());
rebuildTextNodes();
}
void State::focusInsertedBlocks(
const BlockContainerPath &container,
int from,
int count) {
for (auto blockIndex = from; blockIndex != from + count; ++blockIndex) {
const auto path = BlockPath{
.container = container,
.index = blockIndex,
};
for (auto i = 0, textCount = textNodeCount(); i != textCount; ++i) {
if (descriptorBelongsToBlock(_textNodes[i], path)
&& setActiveTextByOrdinal(i)) {
return;
}
}
}
ensureActiveTextOrdinal();
}
State::BoundaryTarget State::destinationTargetForInsertedBlocks(
const BlockContainerPath &container,
int from,
int count) {
focusInsertedBlocks(container, from, count);
if (const auto descriptor = textNode(_activeTextOrdinal)) {
for (auto blockIndex = from; blockIndex != from + count; ++blockIndex) {
const auto path = BlockPath{
.container = container,
.index = blockIndex,
};
if (descriptorBelongsToBlock(*descriptor, path)) {
return {
.action = BoundaryTarget::Action::Text,
.textOrdinal = _activeTextOrdinal,
};
}
}
}
for (auto blockIndex = from; blockIndex != from + count; ++blockIndex) {
const auto path = BlockPath{
.container = container,
.index = blockIndex,
};
if (const auto owner = block(path); owner && CanEditBlock(*owner)) {
return {
.action = BoundaryTarget::Action::StructuralSelection,
.structuralSelection = preparedSelectionForBlock(path),
};
}
}
return {};
}
State::BoundaryTarget State::destinationTargetForInsertedListItems(
const BlockPath &path,
int from,
int count) {
for (auto i = 0, textCount = textNodeCount(); i != textCount; ++i) {
const auto itemIndex = ListItemIndexForLeaf(_textNodes[i].leaf, path);
if (!itemIndex
|| *itemIndex < from
|| *itemIndex >= from + count
|| !setActiveTextByOrdinal(i)) {
continue;
}
return {
.action = BoundaryTarget::Action::Text,
.textOrdinal = _activeTextOrdinal,
};
}
const auto owner = block(path);
if (owner
&& owner->kind == BlockKind::List
&& from >= 0
&& from < int(owner->listItems.size())
&& CanEditBlocks(owner->listItems[from].blocks)) {
ensureActiveTextOrdinal();
return {
.action = BoundaryTarget::Action::StructuralSelection,
.structuralSelection = preparedSelectionForListItem(path, from),
};
}
ensureActiveTextOrdinal();
return (_activeTextOrdinal >= 0)
? BoundaryTarget{
.action = BoundaryTarget::Action::Text,
.textOrdinal = _activeTextOrdinal,
}
: BoundaryTarget();
}
std::optional<int> State::adjacentEditableOrdinal(bool forward) const {
if (_activeTextOrdinal < 0) {
return std::nullopt;
}
const auto ordinal = _activeTextOrdinal + (forward ? 1 : -1);
return (ordinal >= 0 && ordinal < textNodeCount())
? std::make_optional(ordinal)
: std::nullopt;
}
void State::collectBoundarySteps(
const std::vector<Block> &blocks,
const BlockContainerPath &container,
bool forward,
std::vector<BoundaryTarget> *steps) const {
const auto collectBlock = [&](int index) {
const auto path = BlockPath{
.container = container,
.index = index,
};
const auto &block = blocks[index];
switch (block.kind) {
case BlockKind::Heading:
case BlockKind::Paragraph:
case BlockKind::Footer:
case BlockKind::Code:
appendBoundaryTextStep({
.kind = LeafKind::BlockText,
.block = path,
}, steps);
break;
case BlockKind::Quote:
if (forward) {
if (block.blocks.empty()) {
appendBoundaryTextStep({
.kind = LeafKind::BlockText,
.block = path,
}, steps);
}
collectBoundarySteps(
block.blocks,
BlockChildrenContainer(path),
forward,
steps);
appendBoundaryTextStep({
.kind = LeafKind::BlockCaption,
.block = path,
}, steps);
} else {
appendBoundaryTextStep({
.kind = LeafKind::BlockCaption,
.block = path,
}, steps);
collectBoundarySteps(
block.blocks,
BlockChildrenContainer(path),
forward,
steps);
if (block.blocks.empty()) {
appendBoundaryTextStep({
.kind = LeafKind::BlockText,
.block = path,
}, steps);
}
}
appendBoundaryBlockStep(path, steps);
break;
case BlockKind::List:
if (forward) {
for (auto j = 0, itemCount = int(block.listItems.size());
j != itemCount;
++j) {
const auto &item = block.listItems[j];
if (!RichTextIsEmpty(item.text) || item.blocks.empty()) {
appendBoundaryTextStep({
.kind = LeafKind::ListItemText,
.block = path,
.listItemIndex = j,
}, steps);
}
if (!item.blocks.empty()) {
collectBoundarySteps(
item.blocks,
ListItemChildrenContainer(path, j),
forward,
steps);
appendBoundaryListItemStep(path, j, steps);
}
}
} else {
for (auto j = int(block.listItems.size()); j != 0; --j) {
const auto itemIndex = j - 1;
const auto &item = block.listItems[itemIndex];
if (!item.blocks.empty()) {
collectBoundarySteps(
item.blocks,
ListItemChildrenContainer(path, itemIndex),
forward,
steps);
}
if (!RichTextIsEmpty(item.text) || item.blocks.empty()) {
appendBoundaryTextStep({
.kind = LeafKind::ListItemText,
.block = path,
.listItemIndex = itemIndex,
}, steps);
}
if (!item.blocks.empty()) {
appendBoundaryListItemStep(path, itemIndex, steps);
}
}
}
break;
case BlockKind::Photo:
case BlockKind::Video:
case BlockKind::Audio:
case BlockKind::Map:
appendBoundaryTextStep({
.kind = LeafKind::BlockCaption,
.block = path,
}, steps);
appendBoundaryBlockStep(path, steps);
break;
case BlockKind::Math:
appendBoundaryTextStep({
.kind = LeafKind::MathFormula,
.block = path,
}, steps);
break;
case BlockKind::Table:
if (forward) {
if (!block.text.text.text.isEmpty()) {
appendBoundaryTextStep({
.kind = LeafKind::BlockText,
.block = path,
}, steps);
}
for (auto j = 0, rowCount = int(block.tableRows.size());
j != rowCount;
++j) {
const auto &row = block.tableRows[j];
for (auto k = 0, cellCount = int(row.cells.size());
k != cellCount;
++k) {
appendBoundaryTextStep({
.kind = LeafKind::TableCellText,
.block = path,
.tableRowIndex = j,
.tableCellIndex = k,
}, steps);
}
}
} else {
for (auto j = int(block.tableRows.size()); j != 0; --j) {
const auto rowIndex = j - 1;
const auto &row = block.tableRows[rowIndex];
for (auto k = int(row.cells.size()); k != 0; --k) {
appendBoundaryTextStep({
.kind = LeafKind::TableCellText,
.block = path,
.tableRowIndex = rowIndex,
.tableCellIndex = k - 1,
}, steps);
}
}
if (!block.text.text.text.isEmpty()) {
appendBoundaryTextStep({
.kind = LeafKind::BlockText,
.block = path,
}, steps);
}
}
appendBoundaryBlockStep(path, steps);
break;
case BlockKind::Details:
if (forward) {
appendBoundaryTextStep({
.kind = LeafKind::BlockText,
.block = path,
}, steps);
collectBoundarySteps(
block.blocks,
BlockChildrenContainer(path),
forward,
steps);
} else {
collectBoundarySteps(
block.blocks,
BlockChildrenContainer(path),
forward,
steps);
appendBoundaryTextStep({
.kind = LeafKind::BlockText,
.block = path,
}, steps);
}
appendBoundaryBlockStep(path, steps);
break;
default: {
const auto before = steps->size();
if (!block.blocks.empty()) {
collectBoundarySteps(
block.blocks,
BlockChildrenContainer(path),
forward,
steps);
}
if (steps->size() == before) {
appendBoundaryBlockStep(path, steps);
}
} break;
}
};
if (forward) {
for (auto i = 0, count = int(blocks.size()); i != count; ++i) {
collectBlock(i);
}
} else {
for (auto i = int(blocks.size()); i != 0; --i) {
collectBlock(i - 1);
}
}
}
void State::appendBoundaryTextStep(
LeafPath leaf,
std::vector<BoundaryTarget> *steps) const {
const auto ordinal = textNodeOrdinal(leaf);
if (ordinal >= 0) {
steps->push_back({
.action = BoundaryAction::Text,
.textOrdinal = ordinal,
});
}
}
void State::appendBoundaryBlockStep(
const BlockPath &path,
std::vector<BoundaryTarget> *steps) const {
const auto owner = block(path);
if (owner && CanEditBlock(*owner)) {
steps->push_back({
.action = BoundaryAction::StructuralSelection,
.structuralSelection = preparedSelectionForBlock(path),
});
}
}
void State::appendBoundaryListItemStep(
const BlockPath &path,
int itemIndex,
std::vector<BoundaryTarget> *steps) const {
const auto owner = block(path);
if (!owner
|| owner->kind != BlockKind::List
|| itemIndex < 0
|| itemIndex >= int(owner->listItems.size())
|| !CanEditBlocks(owner->listItems[itemIndex].blocks)) {
return;
}
steps->push_back({
.action = BoundaryAction::StructuralSelection,
.structuralSelection = preparedSelectionForListItem(path, itemIndex),
});
}
PreparedEditSelection State::preparedSelectionForBlock(
const BlockPath &path) const {
return {
.kind = PreparedEditSelectionKind::Blocks,
.blocks = {
.container = ToPreparedBlockContainerPath(path.container),
.from = path.index,
.till = path.index + 1,
},
};
}
PreparedEditSelection State::preparedSelectionForListItem(
const BlockPath &path,
int itemIndex) const {
return {
.kind = PreparedEditSelectionKind::ListItems,
.listItems = {
.block = PreparedBlockPath{
.container = ToPreparedBlockContainerPath(path.container),
.index = path.index,
},
.from = itemIndex,
.till = itemIndex + 1,
},
};
}
bool State::descriptorBelongsToBlock(
const TextNodeDescriptor &descriptor,
const BlockPath &path) const {
return leafBelongsToBlock(descriptor.leaf, path);
}
bool State::removalTargetIsEmpty(const RemovalTarget &target) const {
switch (target.kind) {
case RemovalKind::Block:
if (const auto owner = block(target.block)) {
return BlockIsEmpty(*owner);
}
return false;
case RemovalKind::ListItem:
if (const auto item = listItem(target.block, target.listItemIndex)) {
return ListItemIsEmpty(*item);
}
return false;
case RemovalKind::TableCell:
if (const auto cell = tableCell(
target.block,
target.tableRowIndex,
target.tableCellIndex)) {
return RichTextIsEmpty(cell->text);
}
return false;
}
return false;
}
bool State::removeTarget(const RemovalTarget &target) {
clearTemporaryDownParagraph();
switch (target.kind) {
case RemovalKind::Block: {
const auto blocks = blockContainer(target.block.container);
if (!blocks
|| target.block.index < 0
|| target.block.index >= int(blocks->size())) {
return false;
}
blocks->erase(blocks->begin() + target.block.index);
return true;
}
case RemovalKind::ListItem: {
const auto owner = block(target.block);
if (!owner
|| target.listItemIndex < 0
|| target.listItemIndex >= int(owner->listItems.size())) {
return false;
}
owner->listItems.erase(owner->listItems.begin() + target.listItemIndex);
return true;
}
case RemovalKind::TableCell: {
const auto cell = tableCell(
target.block,
target.tableRowIndex,
target.tableCellIndex);
if (!cell) {
return false;
}
cell->text = RichText();
return true;
}
}
return false;
}
void State::normalizeInsertedBlockAnchors(std::vector<Block> &blocks) {
for (auto &block : blocks) {
normalizeInsertedBlockAnchors(blocks, block);
}
}
void State::normalizeInsertedBlockAnchors(
std::vector<Block> &root,
Block &block) {
const auto normalize = [&](QString &id) {
if (id.isEmpty()) {
return;
}
const auto base = id;
id.clear();
auto candidate = base;
for (auto suffix = 2;
anchorIdExists(candidate) || anchorIdExists(root, candidate);
++suffix) {
candidate = base + u"-%1"_q.arg(suffix);
}
id = std::move(candidate);
};
normalize(block.anchorId);
normalizeInsertedRichTextAnchors(root, block.text);
normalizeInsertedRichTextAnchors(root, block.caption);
for (auto &child : block.blocks) {
normalizeInsertedBlockAnchors(root, child);
}
for (auto &item : block.listItems) {
normalize(item.anchorId);
normalizeInsertedRichTextAnchors(root, item.text);
for (auto &child : item.blocks) {
normalizeInsertedBlockAnchors(root, child);
}
}
for (auto &row : block.tableRows) {
for (auto &cell : row.cells) {
normalizeInsertedRichTextAnchors(root, cell.text);
}
}
}
void State::normalizeInsertedRichTextAnchors(
std::vector<Block> &root,
RichText &text) {
const auto normalize = [&](QString &id) {
if (id.isEmpty()) {
return;
}
const auto base = id;
id.clear();
auto candidate = base;
for (auto suffix = 2;
anchorIdExists(candidate) || anchorIdExists(root, candidate);
++suffix) {
candidate = base + u"-%1"_q.arg(suffix);
}
id = std::move(candidate);
};
normalize(text.anchorId);
for (auto &id : text.anchorIds) {
normalize(id);
}
}
bool State::anchorIdExists(const QString &id) const {
return anchorIdExists(_richPage->blocks, id);
}
bool State::anchorIdExists(
const std::vector<Block> &blocks,
const QString &id) const {
for (const auto &block : blocks) {
if (block.anchorId == id
|| block.text.anchorId == id
|| block.caption.anchorId == id
|| ranges::contains(block.text.anchorIds, id)
|| ranges::contains(block.caption.anchorIds, id)) {
return true;
}
if (anchorIdExists(block.blocks, id)) {
return true;
}
for (const auto &item : block.listItems) {
if (item.anchorId == id
|| item.text.anchorId == id
|| ranges::contains(item.text.anchorIds, id)) {
return true;
}
if (anchorIdExists(item.blocks, id)) {
return true;
}
}
for (const auto &row : block.tableRows) {
for (const auto &cell : row.cells) {
if (cell.text.anchorId == id
|| ranges::contains(cell.text.anchorIds, id)) {
return true;
}
}
}
}
return false;
}
QString State::nextAnchorId() const {
for (auto i = 1;; ++i) {
auto result = u"anchor-%1"_q.arg(i);
if (!anchorIdExists(result)) {
return result;
}
}
}
Block State::makeBlock(InsertAction action) const {
switch (action.type) {
case InsertBlockType::Heading:
return MakeHeadingBlock(action.headingLevel);
case InsertBlockType::Blockquote:
return MakeQuoteBlock(false);
case InsertBlockType::Code:
return MakeCodeBlock();
case InsertBlockType::Math:
return MakeMathBlock();
case InsertBlockType::Footer:
return MakeFooterBlock();
case InsertBlockType::Divider:
return MakeDividerBlock();
case InsertBlockType::Anchor:
return MakeAnchorBlock(nextAnchorId());
case InsertBlockType::OrderedList:
return MakeListBlock(ListKind::Ordered);
case InsertBlockType::BulletList:
return MakeListBlock(ListKind::Bullet);
case InsertBlockType::TaskList:
return MakeListBlock(ListKind::Bullet, TaskState::Unchecked);
case InsertBlockType::Pullquote:
return MakeQuoteBlock(true);
case InsertBlockType::Photo:
return MakeMediaBlock(BlockKind::Photo);
case InsertBlockType::Video:
return MakeMediaBlock(BlockKind::Video);
case InsertBlockType::Audio:
return MakeMediaBlock(BlockKind::Audio);
case InsertBlockType::Details:
return MakeDetailsBlock();
case InsertBlockType::Table:
return MakeTableBlock();
case InsertBlockType::Map:
return MakeMapBlock(action.latitude, action.longitude);
}
return MakeParagraphBlock();
}
TextWithEntities State::MakeText(QString text) {
auto result = TextWithEntities();
result.text = std::move(text);
return result;
}
Block State::MakeParagraphBlock() {
auto block = Block();
block.kind = BlockKind::Paragraph;
return block;
}
Block State::MakeFooterBlock() {
auto block = Block();
block.kind = BlockKind::Footer;
return block;
}
Block State::MakeHeadingBlock(int level) {
auto block = Block();
block.kind = BlockKind::Heading;
block.headingLevel = std::clamp(level, 1, 6);
return block;
}
Block State::MakeQuoteBlock(bool pullquote) {
auto block = Block();
block.kind = BlockKind::Quote;
block.pullquote = pullquote;
return block;
}
Block State::MakeCodeBlock() {
auto block = Block();
block.kind = BlockKind::Code;
return block;
}
Block State::MakeMathBlock() {
auto block = Block();
block.kind = BlockKind::Math;
return block;
}
Block State::MakeDividerBlock() {
auto block = Block();
block.kind = BlockKind::Divider;
return block;
}
Block State::MakeAnchorBlock(QString anchorId) {
auto block = Block();
block.kind = BlockKind::Anchor;
block.anchorId = std::move(anchorId);
return block;
}
Block State::MakeListBlock(ListKind kind, TaskState taskState) {
auto block = Block();
block.kind = BlockKind::List;
block.listKind = kind;
auto item = ListItem();
item.taskState = taskState;
block.listItems.push_back(std::move(item));
if (kind != ListKind::Ordered) {
block.orderedList = {};
}
return block;
}
ListItem State::MakeParagraphListItem(TaskState taskState) {
auto item = ListItem();
item.taskState = taskState;
item.number = {};
item.blocks.push_back(MakeParagraphBlock());
return item;
}
Block State::MakeDetailsBlock() {
auto block = Block();
block.kind = BlockKind::Details;
block.blocks.push_back(MakeParagraphBlock());
return block;
}
Block State::MakeTableBlock() {
auto block = Block();
block.kind = BlockKind::Table;
block.bordered = true;
block.tableRows.reserve(3);
for (auto rowIndex = 0; rowIndex != 3; ++rowIndex) {
auto row = RichPage::TableRow();
row.cells.reserve(3);
for (auto cellIndex = 0; cellIndex != 3; ++cellIndex) {
auto cell = TableCell();
cell.header = (rowIndex == 0);
row.cells.push_back(std::move(cell));
}
block.tableRows.push_back(std::move(row));
}
return block;
}
Block State::MakeMediaBlock(BlockKind kind) {
auto block = Block();
block.kind = kind;
return block;
}
Block State::MakeMapBlock(double latitude, double longitude) {
auto block = Block();
block.kind = BlockKind::Map;
block.latitude = latitude;
block.longitude = longitude;
return block;
}
bool State::RichTextIsEmpty(const RichText &text) {
return StringIsEmpty(text.text.text)
&& text.anchorId.isEmpty()
&& text.anchorIds.empty();
}
bool State::ListItemIsEmpty(const ListItem &item) {
if (!RichTextIsEmpty(item.text) || !item.anchorId.isEmpty()) {
return false;
}
for (const auto &block : item.blocks) {
if (!BlockIsEmpty(block)) {
return false;
}
}
return true;
}
bool State::BlockIsEmpty(const Block &block) {
if (!RichTextIsEmpty(block.text)
|| !RichTextIsEmpty(block.caption)
|| !StringIsEmpty(block.formula)
|| !block.language.isEmpty()
|| !block.anchorId.isEmpty()
|| !block.url.isEmpty()
|| !block.html.isEmpty()
|| !block.author.isEmpty()
|| !block.username.isEmpty()
|| !block.channelTitle.isEmpty()
|| !block.audioTitle.isEmpty()
|| !block.audioPerformer.isEmpty()
|| !block.audioFileName.isEmpty()
|| block.photo
|| block.document
|| block.peer
|| block.photoId
|| block.documentId
|| block.channelId
|| block.accessHash
|| block.latitude != 0.
|| block.longitude != 0.
|| !block.mediaItems.empty()) {
return false;
}
for (const auto &child : block.blocks) {
if (!BlockIsEmpty(child)) {
return false;
}
}
for (const auto &item : block.listItems) {
if (!ListItemIsEmpty(item)) {
return false;
}
}
for (const auto &row : block.tableRows) {
for (const auto &cell : row.cells) {
if (!RichTextIsEmpty(cell.text)) {
return false;
}
}
}
return true;
}
bool State::StripWrapperEntityInEditMode(EntityType type) {
switch (type) {
case EntityType::Url:
case EntityType::Email:
case EntityType::Hashtag:
case EntityType::Cashtag:
case EntityType::Mention:
case EntityType::BotCommand:
case EntityType::Phone:
case EntityType::BankCard:
return true;
default:
return false;
}
}
TextWithEntities State::StripEditModeWrapperEntities(TextWithEntities text) {
auto filtered = EntitiesInText();
filtered.reserve(text.entities.size());
for (const auto &entity : text.entities) {
if (!StripWrapperEntityInEditMode(entity.type())) {
filtered.push_back(entity);
}
}
text.entities = std::move(filtered);
return text;
}
bool CanEditRichPage(const RichPage &page) {
return CanEditBlocks(page.blocks);
}
bool CanEditRichPage(const std::shared_ptr<const RichPage> &page) {
return page && CanEditRichPage(*page);
}
} // namespace Iv::Editor