mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-08-06 20:03:12 +00:00
627 lines
17 KiB
C++
627 lines
17 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/markdown/iv_markdown_article_selection.h"
|
|
#include "lang/lang_keys.h"
|
|
|
|
#include <algorithm>
|
|
|
|
namespace Iv::Markdown {
|
|
namespace {
|
|
|
|
constexpr auto kCodeTabColumns = 4;
|
|
const auto kPhotoCopyLabel = u"Photo"_q;
|
|
|
|
[[nodiscard]] TextForMimeData CopyTextForDisplayMath(const LaidOutBlock &block) {
|
|
return TextForMimeData::Simple(u"$$"_q + block.copyText + u"$$"_q);
|
|
}
|
|
|
|
[[nodiscard]] TextForMimeData CopyTextForCodeBlock(
|
|
const LaidOutBlock &block,
|
|
TextSelection selection = AllTextSelection) {
|
|
if (selection == AllTextSelection) {
|
|
auto rich = tr::marked(block.copyText);
|
|
if (!rich.text.isEmpty()) {
|
|
rich.entities.push_back(EntityInText(
|
|
EntityType::Pre,
|
|
0,
|
|
rich.text.size(),
|
|
block.codeLanguage));
|
|
}
|
|
return TextForMimeData::Rich(std::move(rich));
|
|
}
|
|
auto from = 0;
|
|
auto to = 0;
|
|
auto displayPosition = 0;
|
|
auto column = 0;
|
|
auto found = false;
|
|
const auto &text = block.copyText;
|
|
for (auto i = 0, count = int(text.size()); i != count; ++i) {
|
|
const auto ch = text[i];
|
|
const auto width = (ch == QChar::Tabulation)
|
|
? (kCodeTabColumns - (column % kCodeTabColumns))
|
|
: 1;
|
|
const auto nextDisplayPosition = displayPosition + width;
|
|
if (selection.to <= displayPosition) {
|
|
break;
|
|
}
|
|
if (selection.from < nextDisplayPosition
|
|
&& selection.to > displayPosition) {
|
|
if (!found) {
|
|
from = i;
|
|
found = true;
|
|
}
|
|
to = i + 1;
|
|
}
|
|
displayPosition = nextDisplayPosition;
|
|
if (Ui::Text::IsNewline(ch)) {
|
|
column = 0;
|
|
} else {
|
|
column += width;
|
|
}
|
|
}
|
|
if (!found || to <= from) {
|
|
return TextForMimeData();
|
|
}
|
|
auto rich = tr::marked(text.mid(from, to - from));
|
|
if (!rich.text.isEmpty()) {
|
|
rich.entities.push_back(EntityInText(
|
|
EntityType::Pre,
|
|
0,
|
|
rich.text.size(),
|
|
block.codeLanguage));
|
|
}
|
|
return TextForMimeData::Rich(std::move(rich));
|
|
}
|
|
|
|
[[nodiscard]] TextForMimeData CopyTextForTable(const LaidOutBlock &block) {
|
|
auto result = TextForMimeData();
|
|
auto firstRow = true;
|
|
for (const auto &row : block.tableRows) {
|
|
if (!firstRow) {
|
|
result.append(u"\n"_q);
|
|
}
|
|
firstRow = false;
|
|
auto firstCell = true;
|
|
for (const auto &cell : row.cells) {
|
|
if (!firstCell) {
|
|
result.append(u"\t"_q);
|
|
}
|
|
firstCell = false;
|
|
result.append(cell.leaf.toTextForMimeData());
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] TextForMimeData CopyTextForMediaBlock(
|
|
const QString &label,
|
|
const Ui::Text::String &captionLeaf) {
|
|
auto result = label.isEmpty()
|
|
? TextForMimeData()
|
|
: TextForMimeData::Simple(label);
|
|
if (!captionLeaf.isEmpty()) {
|
|
if (!result.empty()) {
|
|
result.append(u"\n"_q);
|
|
}
|
|
result.append(captionLeaf.toTextForMimeData());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] TextForMimeData CopyTextForSingleMediaBlock(
|
|
const LaidOutBlock &block,
|
|
const QString &fallback = QString()) {
|
|
const auto label = !block.copyText.isEmpty()
|
|
? block.copyText
|
|
: !block.labelText.isEmpty()
|
|
? block.labelText
|
|
: fallback;
|
|
return CopyTextForMediaBlock(label, block.leaf);
|
|
}
|
|
|
|
[[nodiscard]] TextForMimeData CopyTextForPhotoBlock(const LaidOutBlock &block) {
|
|
return CopyTextForSingleMediaBlock(block, kPhotoCopyLabel);
|
|
}
|
|
|
|
[[nodiscard]] TextForMimeData CopyTextForPlaceholderBlock(
|
|
const LaidOutBlock &block) {
|
|
return CopyTextForMediaBlock(
|
|
block.labelText.isEmpty() ? block.copyText : block.labelText,
|
|
block.leaf);
|
|
}
|
|
|
|
[[nodiscard]] int AddSelectableSegment(
|
|
std::vector<SelectableSegment> *segments,
|
|
SelectableSegment segment) {
|
|
segment.index = int(segments->size());
|
|
segment.length = std::max(segment.length, 0);
|
|
segments->push_back(std::move(segment));
|
|
return segment.index;
|
|
}
|
|
|
|
[[nodiscard]] int CompareSelectionPositions(
|
|
MarkdownArticleSelectionPosition a,
|
|
MarkdownArticleSelectionPosition b) {
|
|
if (a.segment != b.segment) {
|
|
return (a.segment < b.segment) ? -1 : 1;
|
|
}
|
|
if (a.offset != b.offset) {
|
|
return (a.offset < b.offset) ? -1 : 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] MarkdownArticleSelection NormalizeSelection(
|
|
MarkdownArticleSelection selection) {
|
|
if (selection.empty()) {
|
|
return {};
|
|
}
|
|
if (CompareSelectionPositions(selection.from, selection.to) > 0) {
|
|
std::swap(selection.from, selection.to);
|
|
}
|
|
return selection;
|
|
}
|
|
|
|
[[nodiscard]] int LastTableCellSegmentIndex(
|
|
const std::vector<SelectableSegment> *segments,
|
|
int tableSegmentIndex) {
|
|
auto result = tableSegmentIndex;
|
|
if (!segments) {
|
|
return result;
|
|
}
|
|
for (const auto &segment : *segments) {
|
|
if (segment.tableSegmentIndex == tableSegmentIndex) {
|
|
result = std::max(result, segment.index);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] std::optional<int> SingleTableCellSelection(
|
|
const PaintSelectionState &selectionState,
|
|
int tableSegmentIndex) {
|
|
if (selectionState.empty()
|
|
|| !selectionState.endpoints
|
|
|| tableSegmentIndex < 0) {
|
|
return std::nullopt;
|
|
}
|
|
const auto normalized = NormalizeSelection(selectionState.selection);
|
|
if (normalized.empty()) {
|
|
return std::nullopt;
|
|
}
|
|
const auto lastCellSegment = LastTableCellSegmentIndex(
|
|
selectionState.segments,
|
|
tableSegmentIndex);
|
|
const auto spansWholeTable = (normalized.from.segment < tableSegmentIndex)
|
|
&& (normalized.to.segment > lastCellSegment);
|
|
auto tableHit = false;
|
|
auto cellSegment = -1;
|
|
auto multipleCells = false;
|
|
const auto consider = [&](MarkdownArticleSelectionEndpoint endpoint) {
|
|
if (!endpoint.valid() || !endpoint.direct) {
|
|
return;
|
|
}
|
|
const auto segment = FindSegment(selectionState.segments, endpoint.segment);
|
|
if (!segment) {
|
|
return;
|
|
}
|
|
if (segment->index == tableSegmentIndex) {
|
|
tableHit = true;
|
|
return;
|
|
}
|
|
if (segment->tableSegmentIndex != tableSegmentIndex) {
|
|
return;
|
|
}
|
|
if (cellSegment < 0) {
|
|
cellSegment = segment->index;
|
|
} else if (cellSegment != segment->index) {
|
|
multipleCells = true;
|
|
}
|
|
};
|
|
consider(selectionState.endpoints->from);
|
|
consider(selectionState.endpoints->to);
|
|
if (tableHit || multipleCells || cellSegment < 0 || spansWholeTable) {
|
|
return std::nullopt;
|
|
}
|
|
return cellSegment;
|
|
}
|
|
|
|
[[nodiscard]] std::optional<TextSelection> BaseTextSelectionForSegment(
|
|
const SelectableSegment &segment,
|
|
MarkdownArticleSelection selection) {
|
|
if (selection.empty() || !segment.isTextLeaf()) {
|
|
return std::nullopt;
|
|
}
|
|
selection = NormalizeSelection(selection);
|
|
if (selection.empty()
|
|
|| selection.from.segment > segment.index
|
|
|| selection.to.segment < segment.index) {
|
|
return std::nullopt;
|
|
}
|
|
auto from = 0;
|
|
auto to = SegmentLength(segment);
|
|
if (selection.from.segment == segment.index) {
|
|
from = selection.from.offset;
|
|
}
|
|
if (selection.to.segment == segment.index) {
|
|
to = selection.to.offset;
|
|
}
|
|
from = std::clamp(from, 0, SegmentLength(segment));
|
|
to = std::clamp(to, 0, SegmentLength(segment));
|
|
if (from >= to) {
|
|
return std::nullopt;
|
|
}
|
|
return TextSelection(uint16(from), uint16(to));
|
|
}
|
|
|
|
[[nodiscard]] bool RangeSelectsWholeSegment(
|
|
const SelectableSegment &segment,
|
|
MarkdownArticleSelection selection) {
|
|
selection = NormalizeSelection(selection);
|
|
if (selection.empty()
|
|
|| selection.from.segment > segment.index
|
|
|| selection.to.segment < segment.index) {
|
|
return false;
|
|
}
|
|
auto from = 0;
|
|
auto to = SegmentLength(segment);
|
|
if (selection.from.segment == segment.index) {
|
|
from = selection.from.offset;
|
|
}
|
|
if (selection.to.segment == segment.index) {
|
|
to = selection.to.offset;
|
|
}
|
|
from = std::clamp(from, 0, SegmentLength(segment));
|
|
to = std::clamp(to, 0, SegmentLength(segment));
|
|
return (from < to);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void CollectSelectableSegments(
|
|
std::vector<LaidOutBlock> *blocks,
|
|
std::vector<SelectableSegment> *segments) {
|
|
if (!blocks) {
|
|
return;
|
|
}
|
|
for (auto &block : *blocks) {
|
|
block.segmentIndex = -1;
|
|
block.secondarySegmentIndex = -1;
|
|
switch (block.kind) {
|
|
case PreparedBlockKind::Paragraph:
|
|
case PreparedBlockKind::Heading:
|
|
case PreparedBlockKind::Details: {
|
|
auto segment = SelectableSegment();
|
|
segment.kind = SelectableSegmentKind::TextLeaf;
|
|
segment.leaf = &block.leaf;
|
|
segment.block = █
|
|
segment.outerRect = (block.kind == PreparedBlockKind::Details)
|
|
? block.headerRect
|
|
: block.textRect;
|
|
segment.textRect = block.textRect;
|
|
segment.textWidth = block.textWidth;
|
|
segment.length = block.leaf.length();
|
|
block.segmentIndex = AddSelectableSegment(
|
|
segments,
|
|
std::move(segment));
|
|
} break;
|
|
case PreparedBlockKind::CodeBlock: {
|
|
auto segment = SelectableSegment();
|
|
segment.kind = SelectableSegmentKind::CodeBlock;
|
|
segment.leaf = &block.leaf;
|
|
segment.block = █
|
|
segment.outerRect = block.outer;
|
|
segment.textRect = block.textRect;
|
|
segment.textWidth = block.textWidth;
|
|
segment.length = block.leaf.length();
|
|
block.segmentIndex = AddSelectableSegment(
|
|
segments,
|
|
std::move(segment));
|
|
} break;
|
|
case PreparedBlockKind::DisplayMath: {
|
|
auto segment = SelectableSegment();
|
|
segment.kind = SelectableSegmentKind::DisplayMath;
|
|
segment.block = █
|
|
segment.outerRect = block.visibleFormulaRect;
|
|
segment.length = 1;
|
|
block.segmentIndex = AddSelectableSegment(
|
|
segments,
|
|
std::move(segment));
|
|
} break;
|
|
case PreparedBlockKind::Table: {
|
|
auto segment = SelectableSegment();
|
|
segment.kind = SelectableSegmentKind::Table;
|
|
segment.block = █
|
|
segment.outerRect = block.visibleTableRect;
|
|
segment.length = 1;
|
|
block.segmentIndex = AddSelectableSegment(
|
|
segments,
|
|
std::move(segment));
|
|
for (auto &row : block.tableRows) {
|
|
for (auto &cell : row.cells) {
|
|
auto cellSegment = SelectableSegment();
|
|
cellSegment.kind = SelectableSegmentKind::TextLeaf;
|
|
cellSegment.leaf = &cell.leaf;
|
|
cellSegment.block = █
|
|
cellSegment.cell = &cell;
|
|
cellSegment.outerRect = cell.outer;
|
|
cellSegment.textRect = cell.textRect;
|
|
cellSegment.textWidth = cell.textWidth;
|
|
cellSegment.align = cell.align;
|
|
cellSegment.length = cell.leaf.length();
|
|
cellSegment.tableSegmentIndex = block.segmentIndex;
|
|
cell.tableSegmentIndex = block.segmentIndex;
|
|
cell.segmentIndex = AddSelectableSegment(
|
|
segments,
|
|
std::move(cellSegment));
|
|
}
|
|
}
|
|
} break;
|
|
case PreparedBlockKind::Placeholder:
|
|
case PreparedBlockKind::Photo:
|
|
case PreparedBlockKind::Video:
|
|
case PreparedBlockKind::Audio:
|
|
case PreparedBlockKind::Map:
|
|
case PreparedBlockKind::Channel:
|
|
case PreparedBlockKind::GroupedMedia: {
|
|
auto segment = SelectableSegment();
|
|
segment.kind = (block.kind == PreparedBlockKind::Photo)
|
|
? SelectableSegmentKind::Photo
|
|
: (block.kind == PreparedBlockKind::Placeholder)
|
|
? SelectableSegmentKind::Placeholder
|
|
: SelectableSegmentKind::Media;
|
|
segment.block = █
|
|
segment.outerRect = block.mediaRect;
|
|
segment.length = 1;
|
|
block.segmentIndex = AddSelectableSegment(
|
|
segments,
|
|
std::move(segment));
|
|
if (!block.textRect.isEmpty() && !block.leaf.isEmpty()) {
|
|
auto textSegment = SelectableSegment();
|
|
textSegment.kind = SelectableSegmentKind::TextLeaf;
|
|
textSegment.leaf = &block.leaf;
|
|
textSegment.block = █
|
|
textSegment.outerRect = block.textRect;
|
|
textSegment.textRect = block.textRect;
|
|
textSegment.textWidth = block.textWidth;
|
|
textSegment.length = block.leaf.length();
|
|
textSegment.mediaSegmentIndex = block.segmentIndex;
|
|
block.secondarySegmentIndex = AddSelectableSegment(
|
|
segments,
|
|
std::move(textSegment));
|
|
}
|
|
} break;
|
|
case PreparedBlockKind::List:
|
|
case PreparedBlockKind::ListItem:
|
|
case PreparedBlockKind::Quote:
|
|
case PreparedBlockKind::Rule:
|
|
break;
|
|
}
|
|
CollectSelectableSegments(&block.children, segments);
|
|
}
|
|
}
|
|
|
|
void CollectAnchors(
|
|
const std::vector<LaidOutBlock> &blocks,
|
|
std::vector<std::pair<QString, int>> *anchors) {
|
|
if (!anchors) {
|
|
return;
|
|
}
|
|
for (const auto &block : blocks) {
|
|
if (!block.anchorId.isEmpty()) {
|
|
anchors->push_back({ block.anchorId, block.outer.top() });
|
|
}
|
|
CollectAnchors(block.children, anchors);
|
|
}
|
|
}
|
|
|
|
const SelectableSegment *FindSegment(
|
|
const std::vector<SelectableSegment> *segments,
|
|
int index) {
|
|
if (!segments || index < 0 || index >= int(segments->size())) {
|
|
return nullptr;
|
|
}
|
|
return &(*segments)[index];
|
|
}
|
|
|
|
int SegmentLength(const SelectableSegment &segment) {
|
|
return std::max(segment.length, 0);
|
|
}
|
|
|
|
bool TableSegmentSelected(
|
|
const PaintSelectionState &selectionState,
|
|
int tableSegmentIndex) {
|
|
if (selectionState.empty() || tableSegmentIndex < 0) {
|
|
return false;
|
|
}
|
|
if (SingleTableCellSelection(selectionState, tableSegmentIndex)) {
|
|
return false;
|
|
}
|
|
const auto normalized = NormalizeSelection(selectionState.selection);
|
|
if (normalized.empty()) {
|
|
return false;
|
|
}
|
|
auto selectedCells = 0;
|
|
auto selectedCellIndex = -1;
|
|
for (const auto &segment : *selectionState.segments) {
|
|
if (segment.tableSegmentIndex != tableSegmentIndex
|
|
|| segment.index == tableSegmentIndex) {
|
|
continue;
|
|
}
|
|
const auto textSelection = BaseTextSelectionForSegment(
|
|
segment,
|
|
normalized);
|
|
if (!textSelection || textSelection->empty()) {
|
|
continue;
|
|
}
|
|
if (++selectedCells == 1) {
|
|
selectedCellIndex = segment.index;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
const auto table = FindSegment(selectionState.segments, tableSegmentIndex);
|
|
if (!table || !RangeSelectsWholeSegment(*table, normalized)) {
|
|
return false;
|
|
}
|
|
if (selectedCells != 1) {
|
|
return true;
|
|
}
|
|
if (normalized.from.segment == tableSegmentIndex
|
|
|| normalized.to.segment == tableSegmentIndex) {
|
|
return true;
|
|
}
|
|
const auto lower = std::min(tableSegmentIndex, selectedCellIndex);
|
|
const auto upper = std::max(tableSegmentIndex, selectedCellIndex);
|
|
return (normalized.from.segment < lower)
|
|
&& (normalized.to.segment > upper);
|
|
}
|
|
|
|
std::optional<TextSelection> TextSelectionForSegment(
|
|
const SelectableSegment &segment,
|
|
const PaintSelectionState &selectionState) {
|
|
if (selectionState.empty()) {
|
|
return std::nullopt;
|
|
}
|
|
if (segment.tableSegmentIndex >= 0) {
|
|
if (const auto singleCell = SingleTableCellSelection(
|
|
selectionState,
|
|
segment.tableSegmentIndex);
|
|
singleCell && *singleCell != segment.index) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
if (segment.tableSegmentIndex >= 0
|
|
&& TableSegmentSelected(selectionState, segment.tableSegmentIndex)) {
|
|
return std::nullopt;
|
|
}
|
|
return BaseTextSelectionForSegment(segment, selectionState.selection);
|
|
}
|
|
|
|
std::optional<TextSelection> TextSelectionForSegmentIndex(
|
|
const PaintSelectionState &selectionState,
|
|
int index) {
|
|
const auto segment = FindSegment(selectionState.segments, index);
|
|
return segment
|
|
? TextSelectionForSegment(*segment, selectionState)
|
|
: std::nullopt;
|
|
}
|
|
|
|
bool WholeSegmentSelected(
|
|
const SelectableSegment &segment,
|
|
const PaintSelectionState &selectionState) {
|
|
if (selectionState.empty() || segment.isTextLeaf()) {
|
|
return false;
|
|
}
|
|
if (segment.kind == SelectableSegmentKind::Table) {
|
|
return TableSegmentSelected(selectionState, segment.index);
|
|
}
|
|
return RangeSelectsWholeSegment(segment, selectionState.selection);
|
|
}
|
|
|
|
bool WholeSegmentSelected(
|
|
const PaintSelectionState &selectionState,
|
|
int index) {
|
|
const auto segment = FindSegment(selectionState.segments, index);
|
|
return segment
|
|
? WholeSegmentSelected(*segment, selectionState)
|
|
: false;
|
|
}
|
|
|
|
TextForMimeData TextForSegment(
|
|
const SelectableSegment &segment,
|
|
TextSelection selection) {
|
|
switch (segment.kind) {
|
|
case SelectableSegmentKind::TextLeaf:
|
|
return segment.leaf
|
|
? segment.leaf->toTextForMimeData(selection)
|
|
: TextForMimeData();
|
|
case SelectableSegmentKind::CodeBlock:
|
|
return segment.block
|
|
? CopyTextForCodeBlock(*segment.block, selection)
|
|
: TextForMimeData();
|
|
case SelectableSegmentKind::DisplayMath:
|
|
return segment.block
|
|
? CopyTextForDisplayMath(*segment.block)
|
|
: TextForMimeData();
|
|
case SelectableSegmentKind::Table:
|
|
return segment.block
|
|
? CopyTextForTable(*segment.block)
|
|
: TextForMimeData();
|
|
case SelectableSegmentKind::Placeholder:
|
|
return segment.block
|
|
? CopyTextForPlaceholderBlock(*segment.block)
|
|
: TextForMimeData();
|
|
case SelectableSegmentKind::Photo:
|
|
return segment.block
|
|
? CopyTextForPhotoBlock(*segment.block)
|
|
: TextForMimeData();
|
|
case SelectableSegmentKind::Media:
|
|
return segment.block
|
|
? CopyTextForSingleMediaBlock(*segment.block)
|
|
: TextForMimeData();
|
|
}
|
|
return TextForMimeData();
|
|
}
|
|
|
|
TextForMimeData TextForSelectedSegments(
|
|
const std::vector<SelectableSegment> &segments,
|
|
MarkdownArticleSelection selection,
|
|
const MarkdownArticleSelectionEndpoints *endpoints) {
|
|
if (selection.empty()) {
|
|
return TextForMimeData();
|
|
}
|
|
const auto selectionState = PaintSelectionState{
|
|
.segments = &segments,
|
|
.selection = selection,
|
|
.endpoints = endpoints,
|
|
};
|
|
auto pieces = std::vector<TextForMimeData>();
|
|
for (const auto &segment : segments) {
|
|
if (segment.isTextLeaf()) {
|
|
if (segment.mediaSegmentIndex >= 0
|
|
&& WholeSegmentSelected(
|
|
selectionState,
|
|
segment.mediaSegmentIndex)) {
|
|
continue;
|
|
}
|
|
if (const auto textSelection = TextSelectionForSegment(
|
|
segment,
|
|
selectionState);
|
|
textSelection && !textSelection->empty()) {
|
|
if (auto text = TextForSegment(segment, *textSelection);
|
|
!text.empty()) {
|
|
pieces.push_back(std::move(text));
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (!WholeSegmentSelected(segment, selectionState)) {
|
|
continue;
|
|
}
|
|
if (auto text = TextForSegment(segment); !text.empty()) {
|
|
pieces.push_back(std::move(text));
|
|
}
|
|
}
|
|
if (pieces.empty()) {
|
|
return TextForMimeData();
|
|
} else if (pieces.size() == 1) {
|
|
return std::move(pieces.front());
|
|
}
|
|
auto result = TextForMimeData();
|
|
for (auto i = 0, count = int(pieces.size()); i != count; ++i) {
|
|
if (i) {
|
|
result.append(u"\n"_q);
|
|
}
|
|
result.append(std::move(pieces[i]));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
} // namespace Iv::Markdown
|