Files
AyuGramDesktop/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_blocks.cpp
T
2026-07-02 17:49:02 +04:00

2116 lines
60 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_prepare_native_blocks.h"
#include "base/unixtime.h"
#include "iv/iv_rich_page.h"
#include "iv/markdown/iv_markdown_prepare_links.h"
#include "lang/lang_keys.h"
#include <algorithm>
#include <limits>
#include <utility>
namespace Iv::Markdown {
namespace {
using RichPage = Iv::RichPage;
using RichPageBlock = Iv::RichPage::Block;
using RichPageBlockKind = Iv::RichPage::BlockKind;
using RichPageListItem = Iv::RichPage::ListItem;
using RichPageRelatedArticle = Iv::RichPage::RelatedArticle;
using RichPageTableCell = Iv::RichPage::TableCell;
using RichPageTableRow = Iv::RichPage::TableRow;
struct NativeIvDepthContext {
int listDepth = 0;
int quoteDepth = 0;
};
[[nodiscard]] int CappedNativeIvListDepth(int depth) {
return std::min(depth, std::max(PrepareLimitsForIv().visualListDepth, 0));
}
[[nodiscard]] int CappedNativeIvQuoteDepth(int depth) {
return std::min(depth, std::max(PrepareLimitsForIv().visualQuoteDepth, 0));
}
[[nodiscard]] bool PrepareCanonicalNativeIvBlocks(
const std::vector<RichPageBlock> &blocks,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state,
PreparedEditBlockContainerPath container,
NativeIvDepthContext depthContext);
[[nodiscard]] QString NativeIvDateText(TimeId date) {
return langDateTimeFull(base::unixtime::parse(date));
}
[[nodiscard]] int ScaleNativeIvFormulaCap(
int cap,
int textSize,
int baseTextSize) {
if (cap <= 0) {
return 0;
}
const auto numerator = int64(cap) * std::max(textSize, 1);
const auto denominator = std::max(baseTextSize, 1);
return std::max(int((numerator + denominator - 1) / denominator), 1);
}
[[nodiscard]] NativeIvRichTextContext NativeIvRichTextContextForTextSize(
int textSize,
const MarkdownPrepareDimensions &dimensions) {
return {
.textSize = textSize,
.renderWidthCap = ScaleNativeIvFormulaCap(
dimensions.displayMathMaxRenderWidth,
textSize,
dimensions.displayMathTextSize),
.renderHeightCap = ScaleNativeIvFormulaCap(
dimensions.displayMathMaxRenderHeight,
textSize,
dimensions.displayMathTextSize),
};
}
[[nodiscard]] int NativeIvFlowTextSize(
PreparedBlockKind kind,
int headingLevel,
const MarkdownPrepareDimensions &dimensions) {
if (kind == PreparedBlockKind::Heading
&& headingLevel >= 1
&& headingLevel <= int(dimensions.headingTextSizes.size())) {
return dimensions.headingTextSizes[headingLevel - 1];
}
return dimensions.bodyTextSize;
}
[[nodiscard]] PreparedLink PrepareNativeIvRelatedArticleLink(
QString url,
uint64 webpageId,
QStringView renderedText) {
auto result = PreparedLink();
if (webpageId) {
result.kind = PreparedLinkKind::InstantViewPage;
result.webpageId = webpageId;
NormalizePreparedUrlLink(&result, url);
} else {
result = ClassifiedLink(0, url, nullptr);
if (result.kind == PreparedLinkKind::RejectedRelative
|| result.kind == PreparedLinkKind::LocalFile) {
result.kind = PreparedLinkKind::External;
NormalizePreparedUrlLink(&result, url);
}
}
FinalizePreparedUrlLink(&result, renderedText);
return result;
}
[[nodiscard]] QString NativeIvDetailsAnchorId(NativeIvPrepareState *state) {
return u"details-"_q + QString::number(++state->nextGeneratedId);
}
void SortPreparedIvRichText(PreparedIvRichText *text) {
SortEntities(&text->text);
}
[[nodiscard]] TextWithEntities StripOneTrailingNewline(TextWithEntities text) {
auto removed = 0;
if (text.text.endsWith(u"\r\n"_q)) {
removed = 2;
} else if (!text.text.isEmpty()) {
const auto last = text.text.back();
if ((last == QChar(u'\n')) || (last == QChar(u'\r'))) {
removed = 1;
}
}
if (!removed) {
return text;
}
const auto newLength = text.text.size() - removed;
text.text.chop(removed);
for (auto &entity : text.entities) {
entity.updateTextEnd(newLength);
}
text.entities.erase(
std::remove_if(
text.entities.begin(),
text.entities.end(),
[](const EntityInText &entity) {
return (entity.length() <= 0);
}),
text.entities.end());
return text;
}
[[nodiscard]] bool RichTextHasMeaningfulContent(
const RichPage::RichText &text) {
return !text.text.text.trimmed().isEmpty()
|| !text.anchorId.isEmpty()
|| !text.anchorIds.empty();
}
[[nodiscard]] PreparedBlock EmptyParagraphBlock() {
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Paragraph;
return block;
}
[[nodiscard]] PreparedBlock PrepareNativeIvEmbedPostFallbackParagraph(
QString url) {
auto block = EmptyParagraphBlock();
if (url.isEmpty()) {
return block;
}
auto link = PreparedLink();
link.index = 1;
link.kind = PreparedLinkKind::External;
NormalizePreparedUrlLink(&link, url);
FinalizePreparedUrlLink(&link, url);
block.text.text = std::move(url);
block.text.entities.push_back(EntityInText(
EntityType::CustomUrl,
0,
block.text.text.size(),
InternalLinkData(link.index)));
block.links.push_back(std::move(link));
return block;
}
[[nodiscard]] PreparedBlock PrepareRuleBlock() {
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Rule;
return block;
}
void WrapPreparedIvRichTextItalic(PreparedIvRichText *prepared) {
if (!prepared || prepared->text.text.isEmpty()) {
return;
}
prepared->text.entities.push_back(EntityInText(
EntityType::Italic,
0,
prepared->text.text.size()));
}
void ApplyNativeIvQuoteEditPlaceholderText(
PreparedBlock *block,
bool quoteAuthor);
bool AppendPreparedQuoteParagraph(
std::vector<PreparedBlock> *result,
PreparedIvRichText prepared,
bool pullquote,
bool supplementary = false,
bool allowEmpty = false,
bool quoteAuthor = false,
std::optional<PreparedEditLeafSource> editLeaf = std::nullopt) {
if (pullquote && !quoteAuthor) {
WrapPreparedIvRichTextItalic(&prepared);
}
const auto count = result->size();
if (!AppendPreparedIvRichBlock(
result,
PreparedBlockKind::Paragraph,
0,
std::move(prepared),
QString(),
allowEmpty,
supplementary,
std::nullopt,
std::move(editLeaf))) {
return false;
}
if (result->size() > count) {
result->back().quoteAuthor = quoteAuthor;
if (pullquote) {
result->back().flowAlignment = TableAlignment::Center;
result->back().pullquote = true;
}
if (allowEmpty) {
ApplyNativeIvQuoteEditPlaceholderText(
&result->back(),
quoteAuthor);
}
}
return true;
}
[[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]] QString PreparedOrderedAlphaText(int value, bool upper) {
if (value <= 0) {
return QString::number(value);
}
auto result = QString();
while (value > 0) {
--value;
result.prepend(QChar((upper ? 'A' : 'a') + (value % 26)));
value /= 26;
}
return result;
}
[[nodiscard]] QString PreparedOrderedRomanText(int value, bool upper) {
if (value <= 0) {
return QString::number(value);
}
struct RomanPart {
int value = 0;
const char *text = nullptr;
};
static constexpr RomanPart kParts[] = {
RomanPart{ 1000, "M" },
RomanPart{ 900, "CM" },
RomanPart{ 500, "D" },
RomanPart{ 400, "CD" },
RomanPart{ 100, "C" },
RomanPart{ 90, "XC" },
RomanPart{ 50, "L" },
RomanPart{ 40, "XL" },
RomanPart{ 10, "X" },
RomanPart{ 9, "IX" },
RomanPart{ 5, "V" },
RomanPart{ 4, "IV" },
RomanPart{ 1, "I" },
};
auto result = QString();
for (const auto &part : kParts) {
while (value >= part.value) {
result += QString::fromLatin1(part.text);
value -= part.value;
}
}
return upper ? result : result.toLower();
}
[[nodiscard]] QString FormatPreparedOrderedMarkerBody(
int value,
PreparedOrderedListType type) {
switch (type) {
case PreparedOrderedListType::LowerAlpha:
return PreparedOrderedAlphaText(value, false);
case PreparedOrderedListType::UpperAlpha:
return PreparedOrderedAlphaText(value, true);
case PreparedOrderedListType::LowerRoman:
return PreparedOrderedRomanText(value, false);
case PreparedOrderedListType::UpperRoman:
return PreparedOrderedRomanText(value, true);
case PreparedOrderedListType::Decimal:
return QString::number(value);
}
return QString::number(value);
}
[[nodiscard]] QString FormatPreparedOrderedMarkerText(
int value,
PreparedOrderedListType type,
ListDelimiter delimiter) {
const auto suffix = (delimiter == ListDelimiter::Parenthesis)
? u")"_q
: u"."_q;
return FormatPreparedOrderedMarkerBody(value, type) + suffix;
}
class NativeIvOrderedMarkerFormatter {
public:
NativeIvOrderedMarkerFormatter(
const PreparedBlock &list,
bool editMode)
: _listType(list.orderedType)
, _delimiter(list.listDelimiter)
, _reversed(list.orderedReversed)
, _nextValue(list.startNumber)
, _editMode(editMode) {
}
void apply(
const RichPageListItem &item,
PreparedBlock *block) {
const auto type = item.number.type.has_value()
? ResolvePreparedOrderedListType(item.number.type)
: _listType;
const auto value = item.number.value.value_or(_nextValue);
const auto raw = item.number.rawText();
block->orderedType = type;
block->orderedReversed = _reversed;
block->orderedNumber = value;
block->articleOrderedMarkerText = raw;
block->orderedMarkerText = _editMode
? FormatPreparedOrderedMarkerText(value, type, _delimiter)
: raw.isEmpty()
? FormatPreparedOrderedMarkerText(value, type, _delimiter)
: QString();
_nextValue = value + (_reversed ? -1 : 1);
}
private:
PreparedOrderedListType _listType = PreparedOrderedListType::Decimal;
ListDelimiter _delimiter = ListDelimiter::Period;
bool _reversed = false;
int _nextValue = 1;
bool _editMode = false;
};
using NativeIvTableOccupancyRow = std::vector<char>;
using NativeIvTableOccupancyGrid = std::vector<NativeIvTableOccupancyRow>;
[[nodiscard]] int NormalizeNativeIvTableSpan(int span) {
return std::max(span, 1);
}
[[nodiscard]] int ClampNativeIvTableRowspan(
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>(
NormalizeNativeIvTableSpan(rawRowspan),
remainingRows));
}
[[nodiscard]] int ClampNativeIvTableColspan(
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>(
NormalizeNativeIvTableSpan(rawColspan),
remainingColumns));
}
[[nodiscard]] bool CanOccupyNativeIvTableSlots(
const NativeIvTableOccupancyGrid &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 FirstAvailableNativeIvTableColumn(
const NativeIvTableOccupancyGrid &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 = ClampNativeIvTableColspan(
colspan,
column,
maxColumns);
if (effectiveColspan <= 0) {
continue;
}
if (CanOccupyNativeIvTableSlots(
occupancy,
row,
column,
rowspan,
effectiveColspan)) {
return column;
}
}
return -1;
}
void MarkNativeIvTableSlots(
NativeIvTableOccupancyGrid *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 NativeIvTableColumnCount(
const NativeIvTableOccupancyGrid &occupancy) {
auto result = 0;
for (const auto &row : occupancy) {
result = std::max(result, int(row.size()));
}
return result;
}
[[maybe_unused]] [[nodiscard]] PreparedEditBlockContainerPath BlockChildrenContainer(
PreparedEditBlockPath block) {
auto result = std::move(block.container);
result.steps.push_back({
.kind = PreparedEditBlockContainerKind::BlockChildren,
.blockIndex = block.index,
});
return result;
}
[[maybe_unused]] [[nodiscard]] PreparedEditBlockContainerPath ListItemChildrenContainer(
PreparedEditBlockPath block,
int listItemIndex) {
auto result = std::move(block.container);
result.steps.push_back({
.kind = PreparedEditBlockContainerKind::ListItemChildren,
.blockIndex = block.index,
.listItemIndex = listItemIndex,
});
return result;
}
[[nodiscard]] PreparedEditBlockSource BlockSource(
PreparedEditBlockPath block) {
return { .path = std::move(block) };
}
[[maybe_unused]] [[nodiscard]] PreparedEditListItemSource ListItemSource(
PreparedEditBlockPath block,
int listItemIndex) {
return {
.block = std::move(block),
.listItemIndex = listItemIndex,
};
}
[[maybe_unused]] [[nodiscard]] PreparedEditTableRowSource TableRowSource(
PreparedEditBlockPath block,
int tableRowIndex) {
return {
.block = std::move(block),
.tableRowIndex = tableRowIndex,
};
}
[[maybe_unused]] [[nodiscard]] PreparedEditTableCellSource TableCellSource(
PreparedEditBlockPath block,
int tableRowIndex,
int tableCellIndex,
int column,
int colspan,
int rowspan) {
return {
.block = std::move(block),
.tableRowIndex = tableRowIndex,
.tableCellIndex = tableCellIndex,
.column = column,
.colspan = colspan,
.rowspan = rowspan,
};
}
[[nodiscard]] PreparedEditLeafSource BlockTextLeafSource(
PreparedEditBlockPath block) {
return {
.kind = PreparedEditLeafKind::BlockText,
.block = std::move(block),
};
}
[[nodiscard]] PreparedEditLeafSource BlockCaptionLeafSource(
PreparedEditBlockPath block) {
return {
.kind = PreparedEditLeafKind::BlockCaption,
.block = std::move(block),
};
}
[[nodiscard]] PreparedEditLeafSource ListItemTextLeafSource(
PreparedEditBlockPath block,
int listItemIndex) {
return {
.kind = PreparedEditLeafKind::ListItemText,
.block = std::move(block),
.listItemIndex = listItemIndex,
};
}
[[maybe_unused]] [[nodiscard]] PreparedEditLeafSource TableCellTextLeafSource(
PreparedEditBlockPath block,
int tableRowIndex,
int tableCellIndex) {
return {
.kind = PreparedEditLeafKind::TableCellText,
.block = std::move(block),
.tableRowIndex = tableRowIndex,
.tableCellIndex = tableCellIndex,
};
}
[[nodiscard]] PreparedEditLeafSource MathFormulaLeafSource(
PreparedEditBlockPath block) {
return {
.kind = PreparedEditLeafKind::MathFormula,
.block = std::move(block),
};
}
void ApplyBlockCaptionEditSource(
PreparedBlock *block,
PreparedEditBlockPath path) {
block->editBlock = BlockSource(path);
block->editLeaf = BlockCaptionLeafSource(path);
}
[[nodiscard]] QString NativeIvEditPlaceholderText(
PreparedBlockKind kind,
PreparedEditLeafKind leafKind) {
switch (leafKind) {
case PreparedEditLeafKind::BlockCaption:
return u"Caption"_q;
case PreparedEditLeafKind::TableCellText:
return u"Cell"_q;
case PreparedEditLeafKind::MathFormula:
return u"x^2 + y^2"_q;
case PreparedEditLeafKind::ListItemText:
return u"Text"_q;
case PreparedEditLeafKind::BlockText:
if (kind == PreparedBlockKind::Table) {
return u"Title"_q;
}
return (kind == PreparedBlockKind::Heading)
|| (kind == PreparedBlockKind::Details)
? u"Header"_q
: u"Text"_q;
}
return QString();
}
void ApplyNativeIvEditPlaceholderText(PreparedBlock *block) {
if (!block->editLeaf) {
return;
} else if (block->quoteAuthor
&& (block->editLeaf->kind == PreparedEditLeafKind::BlockCaption)) {
block->editPlaceholderText = u"Add author"_q;
return;
}
block->editPlaceholderText = NativeIvEditPlaceholderText(
block->kind,
block->editLeaf->kind);
}
void ApplyNativeIvQuoteEditPlaceholderText(
PreparedBlock *block,
bool quoteAuthor) {
block->editPlaceholderText = quoteAuthor
? u"Add author"_q
: u"Enter quote"_q;
}
void RefreshPreparedNativeIvQuotePlaceholder(
PreparedBlock *block,
NativeIvPrepareState *state) {
block->editPlaceholderText = QString();
if (state->editMode && block->editLeaf) {
ApplyNativeIvQuoteEditPlaceholderText(block, block->quoteAuthor);
}
}
void ApplyNativeIvEditPlaceholderText(PreparedTableCell *cell) {
if (!cell->editLeaf) {
return;
}
cell->editPlaceholderText = NativeIvEditPlaceholderText(
PreparedBlockKind::Table,
cell->editLeaf->kind);
}
[[nodiscard]] const std::vector<RichPageBlock> *ResolveCanonicalNativeIvContainer(
const RichPage &page,
const PreparedEditBlockContainerPath &container) {
auto blocks = &page.blocks;
for (const auto &step : container.steps) {
if (step.kind == PreparedEditBlockContainerKind::Root) {
blocks = &page.blocks;
continue;
}
if (step.blockIndex < 0 || step.blockIndex >= int(blocks->size())) {
return nullptr;
}
const auto &block = (*blocks)[step.blockIndex];
switch (step.kind) {
case PreparedEditBlockContainerKind::BlockChildren:
blocks = &block.blocks;
break;
case PreparedEditBlockContainerKind::ListItemChildren:
if (step.listItemIndex < 0
|| step.listItemIndex >= int(block.listItems.size())) {
return nullptr;
}
blocks = &block.listItems[step.listItemIndex].blocks;
break;
case PreparedEditBlockContainerKind::Root:
blocks = &page.blocks;
break;
}
}
return blocks;
}
[[nodiscard]] const RichPageBlock *ResolveCanonicalNativeIvBlock(
const RichPage &page,
const PreparedEditBlockPath &path) {
const auto container = ResolveCanonicalNativeIvContainer(page, path.container);
if (!container || path.index < 0 || path.index >= int(container->size())) {
return nullptr;
}
return &(*container)[path.index];
}
[[nodiscard]] PreparedBlock *FindPreparedNativeIvBlockByPath(
std::vector<PreparedBlock> *blocks,
const PreparedEditBlockPath &path) {
for (auto &block : *blocks) {
if (block.editBlock && block.editBlock->path == path) {
return &block;
}
if (const auto nested = FindPreparedNativeIvBlockByPath(
&block.children,
path)) {
return nested;
}
}
return nullptr;
}
[[nodiscard]] PreparedBlock *FindPreparedNativeIvLeafBlock(
PreparedBlock *block,
const PreparedEditLeafSource &source) {
if (!block) {
return nullptr;
}
if (block->editLeaf && (*block->editLeaf == source)) {
return block;
}
for (auto &child : block->children) {
if (const auto nested = FindPreparedNativeIvLeafBlock(&child, source)) {
return nested;
}
}
return nullptr;
}
[[nodiscard]] PreparedTableCell *FindPreparedNativeIvLeafCell(
PreparedBlock *block,
const PreparedEditLeafSource &source) {
if (!block) {
return nullptr;
}
for (auto &row : block->tableRows) {
for (auto &cell : row.cells) {
if (cell.editLeaf && (*cell.editLeaf == source)) {
return &cell;
}
}
}
return nullptr;
}
void StripPreparedDetailsSummaryLinks(PreparedIvRichText *summary) {
if (summary->links.empty()) {
return;
}
const auto from = std::remove_if(
summary->text.entities.begin(),
summary->text.entities.end(),
[](const EntityInText &entity) {
return entity.type() == EntityType::CustomUrl;
});
summary->text.entities.erase(from, summary->text.entities.end());
summary->links.clear();
}
void RefreshPreparedNativeIvBlockPlaceholder(
PreparedBlock *block,
NativeIvPrepareState *state) {
block->editPlaceholderText = QString();
if (state->editMode && block->editLeaf) {
ApplyNativeIvEditPlaceholderText(block);
}
}
void RefreshPreparedNativeIvCellPlaceholder(
PreparedTableCell *cell,
NativeIvPrepareState *state) {
cell->editPlaceholderText = QString();
if (state->editMode && cell->editLeaf) {
ApplyNativeIvEditPlaceholderText(cell);
}
}
void RefreshPreparedNativeIvMediaCaptionPlaceholder(
PreparedBlock *block,
NativeIvPrepareState *state) {
block->editPlaceholderText = QString();
if (state->editMode && block->editLeaf) {
ApplyNativeIvEditPlaceholderText(block);
}
}
void RefreshPreparedNativeIvPlaceholderCopyText(PreparedBlock *block) {
if (block->kind != PreparedBlockKind::Placeholder) {
return;
}
block->placeholder.copyText = block->text.text.isEmpty()
? block->placeholder.label
: (block->placeholder.label + u"\n"_q + block->text.text);
}
[[nodiscard]] bool PreparePreparedNativeIvRichText(
const RichPage::RichText &text,
PreparedIvRichText *prepared,
NativeIvPrepareState *state,
NativeIvRichTextContext context = {}) {
return PrepareNativeIvRichText(text, prepared, nullptr, state, context);
}
[[nodiscard]] NativeInstantViewLeafUpdateResult UpdatePreparedNativeIvBlockText(
PreparedBlock *preparedBlock,
const RichPageBlock &canonicalBlock,
const PreparedEditLeafSource &source,
NativeIvPrepareState *state,
NativeIvPreparedLeafFormulaRange *formulaRange) {
formulaRange->from = state->nextFormulaIndex;
formulaRange->till = state->nextFormulaIndex;
switch (canonicalBlock.kind) {
case RichPageBlockKind::Heading:
case RichPageBlockKind::Paragraph:
case RichPageBlockKind::Footer: {
if (!preparedBlock->editLeaf || (*preparedBlock->editLeaf != source)) {
return NativeInstantViewLeafUpdateResult::Unsupported;
}
auto prepared = PreparedIvRichText();
const auto kind = (canonicalBlock.kind == RichPageBlockKind::Heading)
? PreparedBlockKind::Heading
: PreparedBlockKind::Paragraph;
const auto context = NativeIvRichTextContextForTextSize(
NativeIvFlowTextSize(
kind,
canonicalBlock.headingLevel,
state->dimensions),
state->dimensions);
if (!PreparePreparedNativeIvRichText(
canonicalBlock.text,
&prepared,
state,
context)) {
return NativeInstantViewLeafUpdateResult::Failed;
}
SortPreparedIvRichText(&prepared);
preparedBlock->text = std::move(prepared.text);
preparedBlock->links = std::move(prepared.links);
RefreshPreparedNativeIvBlockPlaceholder(preparedBlock, state);
} break;
case RichPageBlockKind::Code: {
if (!preparedBlock->editLeaf || (*preparedBlock->editLeaf != source)) {
return NativeInstantViewLeafUpdateResult::Unsupported;
}
auto prepared = PreparedIvRichText();
if (!PreparePreparedNativeIvRichText(
canonicalBlock.text,
&prepared,
state,
{ .dropClickHandlers = true })) {
return NativeInstantViewLeafUpdateResult::Failed;
}
SortPreparedIvRichText(&prepared);
preparedBlock->text = StripOneTrailingNewline(std::move(prepared.text));
preparedBlock->links = std::move(prepared.links);
RefreshPreparedNativeIvBlockPlaceholder(preparedBlock, state);
} break;
case RichPageBlockKind::Quote: {
const auto target = FindPreparedNativeIvLeafBlock(preparedBlock, source);
if (!target) {
return NativeInstantViewLeafUpdateResult::Unsupported;
}
auto prepared = PreparedIvRichText();
if (!PreparePreparedNativeIvRichText(
canonicalBlock.text,
&prepared,
state)) {
return NativeInstantViewLeafUpdateResult::Failed;
}
if (canonicalBlock.pullquote) {
WrapPreparedIvRichTextItalic(&prepared);
}
SortPreparedIvRichText(&prepared);
target->text = std::move(prepared.text);
target->links = std::move(prepared.links);
RefreshPreparedNativeIvQuotePlaceholder(target, state);
} break;
case RichPageBlockKind::Table: {
if (!preparedBlock->editLeaf || (*preparedBlock->editLeaf != source)) {
return NativeInstantViewLeafUpdateResult::Unsupported;
}
auto prepared = PreparedIvRichText();
if (!PreparePreparedNativeIvRichText(
canonicalBlock.text,
&prepared,
state)) {
return NativeInstantViewLeafUpdateResult::Failed;
}
SortPreparedIvRichText(&prepared);
preparedBlock->text = std::move(prepared.text);
preparedBlock->links = std::move(prepared.links);
} break;
case RichPageBlockKind::Details: {
if (!preparedBlock->editLeaf || (*preparedBlock->editLeaf != source)) {
return NativeInstantViewLeafUpdateResult::Unsupported;
}
auto prepared = PreparedIvRichText();
if (!PreparePreparedNativeIvRichText(
canonicalBlock.text,
&prepared,
state)) {
return NativeInstantViewLeafUpdateResult::Failed;
}
StripPreparedDetailsSummaryLinks(&prepared);
SortPreparedIvRichText(&prepared);
preparedBlock->text = std::move(prepared.text);
preparedBlock->links = std::move(prepared.links);
RefreshPreparedNativeIvBlockPlaceholder(preparedBlock, state);
} break;
case RichPageBlockKind::Math: {
if (!preparedBlock->editLeaf || (*preparedBlock->editLeaf != source)) {
return NativeInstantViewLeafUpdateResult::Unsupported;
}
if (canonicalBlock.formula.trimmed().isEmpty() && !state->editMode) {
return NativeInstantViewLeafUpdateResult::Unsupported;
}
auto formulaIndex = preparedBlock->formulaIndex;
if (formulaIndex >= 0 && formulaIndex < int(state->result.formulas.size())) {
auto slot = PreparedFormulaSlot();
slot.trimmedTex = canonicalBlock.formula.trimmed();
slot.kind = MathKind::Display;
slot.textSize = state->dimensions.displayMathTextSize;
slot.renderWidthCap = state->dimensions.displayMathMaxRenderWidth;
slot.renderHeightCap = state->dimensions.displayMathMaxRenderHeight;
slot.present = true;
state->result.formulas[formulaIndex] = std::move(slot);
} else {
auto prepared = PreparedBlock();
prepared.kind = PreparedBlockKind::DisplayMath;
prepared.formulaTex = canonicalBlock.formula;
prepared.mathKind = MathKind::Display;
formulaIndex = state->rememberFormula(prepared);
}
preparedBlock->formulaTex = canonicalBlock.formula;
preparedBlock->formulaIndex = formulaIndex;
RefreshPreparedNativeIvBlockPlaceholder(preparedBlock, state);
formulaRange->from = formulaIndex;
formulaRange->till = formulaIndex + 1;
} break;
default:
return NativeInstantViewLeafUpdateResult::Unsupported;
}
if (canonicalBlock.kind != RichPageBlockKind::Math) {
formulaRange->till = state->nextFormulaIndex;
}
return NativeInstantViewLeafUpdateResult::Updated;
}
[[nodiscard]] NativeInstantViewLeafUpdateResult UpdatePreparedNativeIvBlockCaption(
PreparedBlock *preparedBlock,
const RichPageBlock &canonicalBlock,
const PreparedEditLeafSource &source,
NativeIvPrepareState *state,
NativeIvPreparedLeafFormulaRange *formulaRange) {
formulaRange->from = state->nextFormulaIndex;
formulaRange->till = state->nextFormulaIndex;
switch (canonicalBlock.kind) {
case RichPageBlockKind::Quote: {
const auto target = FindPreparedNativeIvLeafBlock(preparedBlock, source);
if (!target) {
return NativeInstantViewLeafUpdateResult::Unsupported;
}
auto prepared = PreparedIvRichText();
if (!PreparePreparedNativeIvRichText(
canonicalBlock.caption,
&prepared,
state)) {
return NativeInstantViewLeafUpdateResult::Failed;
}
SortPreparedIvRichText(&prepared);
target->text = std::move(prepared.text);
target->links = std::move(prepared.links);
RefreshPreparedNativeIvQuotePlaceholder(target, state);
} break;
case RichPageBlockKind::Photo:
case RichPageBlockKind::Video:
case RichPageBlockKind::Audio:
case RichPageBlockKind::Map: {
if (!preparedBlock->editLeaf || (*preparedBlock->editLeaf != source)) {
return NativeInstantViewLeafUpdateResult::Unsupported;
}
auto prepared = PreparedIvRichText();
if (!PreparePreparedNativeIvRichText(
canonicalBlock.caption,
&prepared,
state)) {
return NativeInstantViewLeafUpdateResult::Failed;
}
SortPreparedIvRichText(&prepared);
preparedBlock->text = std::move(prepared.text);
preparedBlock->links = std::move(prepared.links);
if (preparedBlock->kind == PreparedBlockKind::Photo) {
preparedBlock->photo.caption = preparedBlock->text;
} else if (preparedBlock->kind == PreparedBlockKind::Video) {
preparedBlock->video.caption = preparedBlock->text;
}
RefreshPreparedNativeIvMediaCaptionPlaceholder(preparedBlock, state);
RefreshPreparedNativeIvPlaceholderCopyText(preparedBlock);
} break;
default:
return NativeInstantViewLeafUpdateResult::Unsupported;
}
formulaRange->till = state->nextFormulaIndex;
return NativeInstantViewLeafUpdateResult::Updated;
}
[[nodiscard]] NativeInstantViewLeafUpdateResult UpdatePreparedNativeIvListItemText(
PreparedBlock *preparedBlock,
const RichPageListItem &canonicalItem,
const PreparedEditLeafSource &source,
NativeIvPrepareState *state,
NativeIvPreparedLeafFormulaRange *formulaRange) {
const auto target = FindPreparedNativeIvLeafBlock(preparedBlock, source);
if (!target) {
return NativeInstantViewLeafUpdateResult::Unsupported;
}
formulaRange->from = state->nextFormulaIndex;
formulaRange->till = state->nextFormulaIndex;
auto prepared = PreparedIvRichText();
if (!PreparePreparedNativeIvRichText(canonicalItem.text, &prepared, state)) {
return NativeInstantViewLeafUpdateResult::Failed;
}
SortPreparedIvRichText(&prepared);
target->text = std::move(prepared.text);
target->links = std::move(prepared.links);
RefreshPreparedNativeIvBlockPlaceholder(target, state);
formulaRange->till = state->nextFormulaIndex;
return NativeInstantViewLeafUpdateResult::Updated;
}
[[nodiscard]] NativeInstantViewLeafUpdateResult UpdatePreparedNativeIvTableCellText(
PreparedBlock *preparedBlock,
const RichPageTableCell &canonicalCell,
const PreparedEditLeafSource &source,
NativeIvPrepareState *state,
NativeIvPreparedLeafFormulaRange *formulaRange) {
const auto target = FindPreparedNativeIvLeafCell(preparedBlock, source);
if (!target) {
return NativeInstantViewLeafUpdateResult::Unsupported;
}
formulaRange->from = state->nextFormulaIndex;
formulaRange->till = state->nextFormulaIndex;
auto prepared = PreparedIvRichText();
const auto context = NativeIvRichTextContextForTextSize(
canonicalCell.header
? state->dimensions.tableHeaderTextSize
: state->dimensions.tableBodyTextSize,
state->dimensions);
if (!PreparePreparedNativeIvRichText(
canonicalCell.text,
&prepared,
state,
context)) {
return NativeInstantViewLeafUpdateResult::Failed;
}
SortPreparedIvRichText(&prepared);
target->text = std::move(prepared.text);
target->links = std::move(prepared.links);
RefreshPreparedNativeIvCellPlaceholder(target, state);
formulaRange->till = state->nextFormulaIndex;
return NativeInstantViewLeafUpdateResult::Updated;
}
using PrepareCanonicalMediaBlock = bool (*)(
const RichPageBlock &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state);
[[nodiscard]] bool PrepareCanonicalNativeIvMediaBlock(
const RichPageBlock &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state,
PreparedEditBlockPath path,
PrepareCanonicalMediaBlock prepare) {
const auto count = result->size();
if (!prepare(data, result, state)) {
return false;
}
if (result->size() > count) {
ApplyBlockCaptionEditSource(&result->back(), std::move(path));
if (state->editMode) {
ApplyNativeIvEditPlaceholderText(&result->back());
}
}
return true;
}
[[nodiscard]] bool PrepareCanonicalNativeIvGroupedMediaBlock(
const RichPageBlock &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state,
PreparedEditBlockPath path) {
const auto count = result->size();
if (!PrepareNativeIvGroupedMediaBlock(data, result, state)) {
return false;
}
if (result->size() > count) {
result->back().editBlock = BlockSource(std::move(path));
}
return true;
}
void ClearPreparedEditSources(std::vector<PreparedBlock> *blocks) {
for (auto &block : *blocks) {
block.editBlock.reset();
block.editListItem.reset();
block.editLeaf.reset();
for (auto &row : block.tableRows) {
row.editRow.reset();
for (auto &cell : row.cells) {
cell.editCell.reset();
cell.editLeaf.reset();
}
}
ClearPreparedEditSources(&block.children);
}
}
[[nodiscard]] bool PrepareNativeIvRichPlaceholderBlock(
QString label,
const RichPage::RichText &caption,
QString anchorId,
std::optional<EmbedRequest> embed,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
auto prepared = PreparedIvRichText();
if (!PrepareNativeIvRichText(
caption,
&prepared,
&anchorId,
state)) {
return state->result.failure.failed()
? false
: PrepareNativeIvPlainPlaceholderBlock(std::move(label), result);
}
SortPreparedIvRichText(&prepared);
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Placeholder;
block.text = std::move(prepared.text);
block.links = std::move(prepared.links);
block.anchorId = std::move(anchorId);
block.anchorIds = std::move(prepared.anchorIds);
block.supplementary = true;
block.placeholder.label = std::move(label);
block.placeholder.copyText = block.text.text.isEmpty()
? block.placeholder.label
: (block.placeholder.label + u"\n"_q + block.text.text);
block.placeholder.embed = std::move(embed);
if (block.placeholder.embed) {
block.placeholder.id = { .value = uint64(++state->nextGeneratedId) };
}
result->push_back(std::move(block));
return true;
}
[[nodiscard]] auto EmbedRequestFromCanonicalBlock(
const RichPageBlock &block) -> std::optional<EmbedRequest> {
auto request = EmbedRequest{
.width = block.width,
.height = block.height,
.fullWidth = block.fullWidth,
.fixedHeight = block.fixedHeight,
.allowScrolling = block.allowScrolling,
};
if (!block.url.isEmpty()) {
request.url = block.url;
} else if (!block.html.isEmpty()) {
request.html = block.html;
} else {
return std::nullopt;
}
return request;
}
[[nodiscard]] bool AppendNativeIvFlowBlock(
std::vector<PreparedBlock> *result,
PreparedBlockKind kind,
int headingLevel,
const RichPage::RichText &text,
QString anchorId,
std::optional<PreparedEditBlockPath> path,
NativeIvPrepareState *state,
bool allowEmpty = false) {
auto prepared = PreparedIvRichText();
const auto context = NativeIvRichTextContextForTextSize(
NativeIvFlowTextSize(kind, headingLevel, state->dimensions),
state->dimensions);
if (!PrepareNativeIvRichText(
text,
&prepared,
&anchorId,
state,
context)) {
return false;
}
auto editBlock = std::optional<PreparedEditBlockSource>();
auto editLeaf = std::optional<PreparedEditLeafSource>();
if (path) {
editBlock = BlockSource(*path);
editLeaf = BlockTextLeafSource(*path);
}
const auto count = result->size();
const auto appended = AppendPreparedIvRichBlock(
result,
kind,
headingLevel,
std::move(prepared),
std::move(anchorId),
allowEmpty || state->editMode,
false,
std::move(editBlock),
std::move(editLeaf));
if (appended && state->editMode && (result->size() > count)) {
ApplyNativeIvEditPlaceholderText(&result->back());
}
return appended;
}
[[nodiscard]] bool PrepareCanonicalNativeIvQuoteBlock(
const RichPageBlock &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state,
PreparedEditBlockPath path,
NativeIvDepthContext depthContext) {
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Quote;
block.anchorId = data.anchorId;
block.pullquote = data.pullquote;
block.actualDepth = depthContext.quoteDepth;
block.visualDepth = CappedNativeIvQuoteDepth(block.actualDepth);
block.depthClamped = (block.actualDepth > block.visualDepth);
block.editBlock = BlockSource(path);
auto body = PreparedIvRichText();
if (!PrepareNativeIvRichText(data.text, &body, &block.anchorId, state)) {
return false;
}
if (data.blocks.empty() && !AppendPreparedQuoteParagraph(
&block.children,
std::move(body),
data.pullquote,
false,
state->editMode,
false,
BlockTextLeafSource(path))) {
return false;
}
auto childContext = depthContext;
++childContext.quoteDepth;
if (!data.blocks.empty() && !PrepareCanonicalNativeIvBlocks(
data.blocks,
&block.children,
state,
BlockChildrenContainer(path),
childContext)) {
return false;
}
auto cite = PreparedIvRichText();
if (!PrepareNativeIvRichText(data.caption, &cite, &block.anchorId, state)) {
return false;
}
const auto quoteAuthorEmpty = cite.text.text.isEmpty();
const auto includeQuoteAuthor = state->editMode || !quoteAuthorEmpty;
if (includeQuoteAuthor && !AppendPreparedQuoteParagraph(
&block.children,
std::move(cite),
data.pullquote,
true,
state->editMode,
true,
BlockCaptionLeafSource(path))) {
return false;
}
if (block.children.empty()) {
block.children.push_back(EmptyParagraphBlock());
if (data.pullquote) {
block.children.back().flowAlignment = TableAlignment::Center;
block.children.back().pullquote = true;
}
}
result->push_back(std::move(block));
return true;
}
[[nodiscard]] TaskState NativeIvTaskStateFromRichPage(
RichPage::TaskState state) {
switch (state) {
case RichPage::TaskState::Unchecked:
return TaskState::Unchecked;
case RichPage::TaskState::Checked:
return TaskState::Checked;
case RichPage::TaskState::None:
break;
}
return TaskState::None;
}
[[nodiscard]] bool PrepareCanonicalNativeIvList(
const std::vector<RichPageListItem> &items,
PreparedBlock *result,
NativeIvPrepareState *state,
PreparedEditBlockPath path,
NativeIvDepthContext depthContext) {
result->actualDepth = depthContext.listDepth;
result->visualDepth = CappedNativeIvListDepth(result->actualDepth);
result->depthClamped = (result->actualDepth > result->visualDepth);
result->editBlock = BlockSource(path);
auto orderedFormatter = std::optional<NativeIvOrderedMarkerFormatter>();
if (result->listKind == ListKind::Ordered) {
orderedFormatter.emplace(*result, state->editMode);
}
for (auto i = 0, count = int(items.size()); i != count; ++i) {
const auto &item = items[i];
auto block = PreparedBlock();
block.kind = PreparedBlockKind::ListItem;
block.listKind = result->listKind;
block.listDelimiter = result->listDelimiter;
block.taskState = NativeIvTaskStateFromRichPage(item.taskState);
block.actualDepth = result->actualDepth;
block.visualDepth = result->visualDepth;
block.depthClamped = result->depthClamped;
block.editListItem = ListItemSource(path, i);
if (orderedFormatter.has_value()) {
orderedFormatter->apply(item, &block);
}
if (RichTextHasMeaningfulContent(item.text)) {
auto prepared = PreparedIvRichText();
auto anchorId = item.anchorId;
if (!PrepareNativeIvRichText(
item.text,
&prepared,
&anchorId,
state)) {
return false;
}
block.anchorId = std::move(anchorId);
if (!AppendPreparedIvRichBlock(
&block.children,
PreparedBlockKind::Paragraph,
0,
std::move(prepared),
QString(),
state->editMode,
false,
std::nullopt,
ListItemTextLeafSource(path, i))) {
return false;
}
if (state->editMode && !block.children.empty()) {
ApplyNativeIvEditPlaceholderText(&block.children.back());
}
}
auto childContext = depthContext;
++childContext.listDepth;
if (!item.blocks.empty() && !PrepareCanonicalNativeIvBlocks(
item.blocks,
&block.children,
state,
ListItemChildrenContainer(path, i),
childContext)) {
return false;
}
if (block.children.empty()) {
block.children.push_back(EmptyParagraphBlock());
block.children.back().editLeaf = ListItemTextLeafSource(path, i);
if (state->editMode) {
ApplyNativeIvEditPlaceholderText(&block.children.back());
}
}
result->children.push_back(std::move(block));
}
return true;
}
[[nodiscard]] TableAlignment NativeIvTableAlignment(
const RichPageTableCell &cell) {
switch (cell.alignment) {
case RichPage::TableAlignment::Center:
return TableAlignment::Center;
case RichPage::TableAlignment::Right:
return TableAlignment::Right;
case RichPage::TableAlignment::Left:
break;
}
return TableAlignment::Left;
}
[[nodiscard]] PreparedTableCellVerticalAlignment NativeIvTableVerticalAlignment(
const RichPageTableCell &cell) {
switch (cell.verticalAlignment) {
case RichPage::TableVerticalAlignment::Middle:
return PreparedTableCellVerticalAlignment::Middle;
case RichPage::TableVerticalAlignment::Bottom:
return PreparedTableCellVerticalAlignment::Bottom;
case RichPage::TableVerticalAlignment::Top:
break;
}
return PreparedTableCellVerticalAlignment::Top;
}
[[nodiscard]] bool PrepareCanonicalNativeIvTableBlock(
const RichPageBlock &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state,
PreparedEditBlockPath path) {
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Table;
block.tableBordered = data.bordered;
block.tableStriped = data.striped;
block.editBlock = BlockSource(path);
auto title = PreparedIvRichText();
block.anchorId = data.anchorId;
if (!PrepareNativeIvRichText(data.text, &title, &block.anchorId, state)) {
return false;
}
SortPreparedIvRichText(&title);
block.text = std::move(title.text);
block.links = std::move(title.links);
block.anchorIds = std::move(title.anchorIds);
if (!block.text.text.isEmpty() || state->editMode) {
block.editLeaf = BlockTextLeafSource(path);
}
if (state->editMode) {
block.forceTextSegment = true;
ApplyNativeIvEditPlaceholderText(&block);
}
const auto &limits = state->tableRenderLimits;
const auto rowCount = std::min(int(data.tableRows.size()), limits.maxRows);
auto occupancy = NativeIvTableOccupancyGrid(rowCount);
block.tableRows.reserve(rowCount);
auto occupiedSlotCountSoFar = int64(0);
for (auto rowIndex = 0; rowIndex != rowCount; ++rowIndex) {
const auto &row = data.tableRows[rowIndex];
auto preparedRow = PreparedTableRow();
preparedRow.editRow = TableRowSource(path, rowIndex);
preparedRow.cells.reserve(std::min(int(row.cells.size()), limits.maxColumns));
for (auto cellIndex = 0, cellCount = int(row.cells.size());
cellIndex != cellCount;
++cellIndex) {
const auto &cell = row.cells[cellIndex];
auto preparedCell = PreparedTableCell();
const auto normalizedColspan = NormalizeNativeIvTableSpan(cell.colspan);
const auto rowspan = ClampNativeIvTableRowspan(
cell.rowspan,
rowIndex,
rowCount);
if (rowspan <= 0) {
continue;
}
const auto column = FirstAvailableNativeIvTableColumn(
occupancy,
rowIndex,
rowspan,
normalizedColspan,
limits.maxColumns);
if (column < 0) {
continue;
}
const auto colspan = ClampNativeIvTableColspan(
normalizedColspan,
column,
limits.maxColumns);
if (colspan <= 0) {
continue;
}
const auto occupiedSlotGrowth = int64(rowspan) * colspan;
if (occupiedSlotGrowth > limits.maxCells
|| (occupiedSlotCountSoFar + occupiedSlotGrowth) > limits.maxCells) {
continue;
}
preparedCell.column = column;
preparedCell.alignment = NativeIvTableAlignment(cell);
preparedCell.header = cell.header;
preparedCell.verticalAlignment = NativeIvTableVerticalAlignment(cell);
preparedCell.colspan = colspan;
preparedCell.rowspan = rowspan;
preparedCell.editCell = TableCellSource(
path,
rowIndex,
cellIndex,
column,
colspan,
rowspan);
preparedCell.editLeaf = TableCellTextLeafSource(
path,
rowIndex,
cellIndex);
if (state->editMode) {
ApplyNativeIvEditPlaceholderText(&preparedCell);
}
if (!cell.text.text.empty()) {
auto rich = PreparedIvRichText();
const auto context = NativeIvRichTextContextForTextSize(
cell.header
? state->dimensions.tableHeaderTextSize
: state->dimensions.tableBodyTextSize,
state->dimensions);
if (!PrepareNativeIvRichText(
cell.text,
&rich,
nullptr,
state,
context)) {
return false;
}
SortPreparedIvRichText(&rich);
preparedCell.text = std::move(rich.text);
preparedCell.links = std::move(rich.links);
}
MarkNativeIvTableSlots(
&occupancy,
rowIndex,
column,
rowspan,
colspan);
occupiedSlotCountSoFar += occupiedSlotGrowth;
preparedRow.cells.push_back(std::move(preparedCell));
}
preparedRow.header = !preparedRow.cells.empty()
&& std::all_of(
preparedRow.cells.begin(),
preparedRow.cells.end(),
[](const PreparedTableCell &cell) {
return cell.header;
});
block.tableRows.push_back(std::move(preparedRow));
}
block.tableColumnCount = NativeIvTableColumnCount(occupancy);
block.tableAlignments.resize(block.tableColumnCount, TableAlignment::Left);
for (const auto &preparedRow : block.tableRows) {
for (const auto &preparedCell : preparedRow.cells) {
const auto from = std::max(preparedCell.column, 0);
const auto to = std::min(
preparedCell.column + preparedCell.colspan,
block.tableColumnCount);
for (auto column = from; column != to; ++column) {
block.tableAlignments[column] = preparedCell.alignment;
}
}
}
result->push_back(std::move(block));
return true;
}
[[nodiscard]] bool PrepareCanonicalNativeIvDetailsBlock(
const RichPageBlock &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state,
PreparedEditBlockPath path,
NativeIvDepthContext depthContext) {
auto summary = PreparedIvRichText();
auto anchorId = NativeIvDetailsAnchorId(state);
if (!PrepareNativeIvRichText(
data.text,
&summary,
&anchorId,
state)) {
return false;
}
if (!summary.links.empty()) {
const auto from = std::remove_if(
summary.text.entities.begin(),
summary.text.entities.end(),
[](const EntityInText &entity) {
return entity.type() == EntityType::CustomUrl;
});
summary.text.entities.erase(from, summary.text.entities.end());
summary.links.clear();
}
SortPreparedIvRichText(&summary);
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Details;
block.anchorId = std::move(anchorId);
block.detailsOpen = data.open;
block.collapsed = state->editMode ? false : !data.open;
block.text = std::move(summary.text);
block.links = std::move(summary.links);
block.anchorIds = std::move(summary.anchorIds);
block.editBlock = BlockSource(path);
block.editLeaf = BlockTextLeafSource(path);
if (state->editMode) {
ApplyNativeIvEditPlaceholderText(&block);
}
return PrepareCanonicalNativeIvBlocks(
data.blocks,
&block.children,
state,
BlockChildrenContainer(path),
depthContext)
? (result->push_back(std::move(block)), true)
: false;
}
[[nodiscard]] QString NativeIvRelatedArticleFooterText(
const RichPageRelatedArticle &article) {
if (article.publishedDate && !article.author.isEmpty()) {
return article.author + u", "_q + NativeIvDateText(article.publishedDate);
} else if (article.publishedDate) {
return NativeIvDateText(article.publishedDate);
}
return article.author;
}
[[nodiscard]] bool PrepareCanonicalNativeIvRelatedArticlesBlock(
const RichPageBlock &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
auto related = std::vector<PreparedBlock>();
related.reserve(data.relatedArticles.size());
for (const auto &article : data.relatedArticles) {
auto prepared = PreparedBlock();
prepared.kind = PreparedBlockKind::RelatedArticle;
prepared.relatedArticle.title = article.title.trimmed();
prepared.relatedArticle.description = article.description.trimmed();
prepared.relatedArticle.footer = NativeIvRelatedArticleFooterText(article);
prepared.relatedArticle.photoId = article.photoId;
if (prepared.relatedArticle.title.isEmpty()
&& prepared.relatedArticle.description.isEmpty()
&& prepared.relatedArticle.footer.isEmpty()) {
prepared.relatedArticle.title = article.url.trimmed();
}
const auto linkText = !prepared.relatedArticle.title.isEmpty()
? prepared.relatedArticle.title
: !prepared.relatedArticle.description.isEmpty()
? prepared.relatedArticle.description
: prepared.relatedArticle.footer;
prepared.relatedArticle.link = PrepareNativeIvRelatedArticleLink(
article.url,
article.webpageId,
linkText);
const auto appendLine = [&](QString *copyText, const QString &line) {
if (line.isEmpty()) {
return;
} else if (!copyText->isEmpty()) {
copyText->append(QChar('\n'));
}
copyText->append(line);
};
appendLine(&prepared.relatedArticle.copyText, prepared.relatedArticle.title);
appendLine(
&prepared.relatedArticle.copyText,
prepared.relatedArticle.description);
appendLine(&prepared.relatedArticle.copyText, prepared.relatedArticle.footer);
if (prepared.relatedArticle.copyText.isEmpty()) {
prepared.relatedArticle.copyText = prepared.relatedArticle.link.target;
}
related.push_back(std::move(prepared));
}
if (related.empty()) {
return true;
}
auto title = PreparedIvRichText();
auto anchorId = data.anchorId;
if (!PrepareNativeIvRichText(data.text, &title, &anchorId, state)) {
return false;
}
SortPreparedIvRichText(&title);
if (!title.text.text.isEmpty()) {
if (!AppendPreparedIvRichBlock(
result,
PreparedBlockKind::Heading,
4,
std::move(title),
std::move(anchorId),
state->editMode)) {
return false;
}
} else if (!anchorId.isEmpty() && related.front().anchorId.isEmpty()) {
related.front().anchorId = std::move(anchorId);
}
result->insert(
result->end(),
std::make_move_iterator(related.begin()),
std::make_move_iterator(related.end()));
return true;
}
[[nodiscard]] bool PrepareCanonicalNativeIvEmbedPostBlock(
const RichPageBlock &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state,
NativeIvDepthContext depthContext) {
auto caption = PreparedIvRichText();
auto block = PreparedBlock();
block.kind = PreparedBlockKind::EmbedPost;
block.embedPost.url = data.url.trimmed();
block.embedPost.authorPhotoId = data.photoId;
block.embedPost.author = data.author.trimmed();
if (data.date) {
block.embedPost.dateText = NativeIvDateText(data.date);
}
auto anchorId = data.anchorId;
if (!PrepareNativeIvRichText(data.caption, &caption, &anchorId, state)) {
return false;
}
SortPreparedIvRichText(&caption);
block.text = std::move(caption.text);
block.links = std::move(caption.links);
block.anchorId = std::move(anchorId);
block.anchorIds = std::move(caption.anchorIds);
block.supplementary = true;
if (data.blocks.empty()) {
block.children.push_back(
PrepareNativeIvEmbedPostFallbackParagraph(block.embedPost.url));
} else if (!PrepareCanonicalNativeIvBlocks(
data.blocks,
&block.children,
state,
PreparedEditBlockContainerPath(),
depthContext)) {
return false;
} else {
ClearPreparedEditSources(&block.children);
}
result->push_back(std::move(block));
return true;
}
[[nodiscard]] bool PrepareCanonicalNativeIvBlock(
const RichPageBlock &block,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state,
PreparedEditBlockPath path,
NativeIvDepthContext depthContext) {
if (state->blocked()) {
return false;
}
switch (block.kind) {
case RichPageBlockKind::Unsupported:
return true;
case RichPageBlockKind::Heading:
return AppendNativeIvFlowBlock(
result,
PreparedBlockKind::Heading,
block.headingLevel,
block.text,
block.anchorId,
path,
state);
case RichPageBlockKind::Paragraph:
case RichPageBlockKind::Footer:
return AppendNativeIvFlowBlock(
result,
PreparedBlockKind::Paragraph,
0,
block.text,
block.anchorId,
path,
state);
case RichPageBlockKind::Thinking:
return AppendNativeIvFlowBlock(
result,
PreparedBlockKind::Thinking,
0,
block.text,
block.anchorId,
std::nullopt,
state);
case RichPageBlockKind::AuthorDate: {
auto prepared = PreparedIvRichText();
auto anchorId = block.anchorId;
if (!PrepareNativeIvRichText(
block.text,
&prepared,
&anchorId,
state)) {
return false;
}
if (block.date) {
if (!prepared.text.text.isEmpty()) {
prepared.text.append(u" \u2022 "_q);
}
prepared.text.append(NativeIvDateText(block.date));
}
return AppendPreparedIvRichBlock(
result,
PreparedBlockKind::Paragraph,
0,
std::move(prepared),
std::move(anchorId),
false,
true);
}
case RichPageBlockKind::Code: {
auto prepared = PreparedIvRichText();
auto anchorId = block.anchorId;
if (!PrepareNativeIvRichText(
block.text,
&prepared,
&anchorId,
state,
{ .dropClickHandlers = true })) {
return false;
}
auto code = PreparedBlock();
code.kind = PreparedBlockKind::CodeBlock;
code.anchorId = std::move(anchorId);
code.codeLanguage = block.language;
SortPreparedIvRichText(&prepared);
code.text = StripOneTrailingNewline(std::move(prepared.text));
code.links = std::move(prepared.links);
code.anchorIds = std::move(prepared.anchorIds);
code.editBlock = BlockSource(path);
code.editLeaf = BlockTextLeafSource(path);
if (state->editMode) {
ApplyNativeIvEditPlaceholderText(&code);
}
result->push_back(std::move(code));
return true;
}
case RichPageBlockKind::Divider: {
auto prepared = PrepareRuleBlock();
prepared.editBlock = BlockSource(path);
result->push_back(std::move(prepared));
return true;
}
case RichPageBlockKind::Anchor: {
auto prepared = PreparedIvRichText();
auto editBlock = std::optional<PreparedEditBlockSource>();
editBlock = BlockSource(path);
return AppendPreparedIvRichBlock(
result,
PreparedBlockKind::Paragraph,
0,
std::move(prepared),
block.anchorId,
true,
false,
std::move(editBlock));
}
case RichPageBlockKind::List: {
auto prepared = PreparedBlock();
prepared.kind = PreparedBlockKind::List;
prepared.listKind = (block.listKind == RichPage::ListKind::Ordered)
? ListKind::Ordered
: ListKind::Bullet;
if (prepared.listKind == ListKind::Ordered) {
prepared.listDelimiter = ListDelimiter::Period;
prepared.orderedType = ResolvePreparedOrderedListType(
block.orderedList.type);
prepared.orderedReversed = block.orderedList.reversed;
prepared.startNumber = block.orderedList.start.value_or(
prepared.orderedReversed
? int(block.listItems.size())
: 1);
}
return PrepareCanonicalNativeIvList(
block.listItems,
&prepared,
state,
path,
depthContext)
? (result->push_back(std::move(prepared)), true)
: false;
}
case RichPageBlockKind::Quote:
return PrepareCanonicalNativeIvQuoteBlock(
block,
result,
state,
path,
depthContext);
case RichPageBlockKind::Photo:
return PrepareCanonicalNativeIvMediaBlock(
block,
result,
state,
path,
PrepareNativeIvPhotoBlock);
case RichPageBlockKind::Video:
return PrepareCanonicalNativeIvMediaBlock(
block,
result,
state,
path,
PrepareNativeIvVideoBlock);
case RichPageBlockKind::Embed:
return PrepareNativeIvRichPlaceholderBlock(
tr::lng_iv_click_to_view(tr::now),
block.caption,
block.anchorId,
EmbedRequestFromCanonicalBlock(block),
result,
state);
case RichPageBlockKind::EmbedPost:
return PrepareCanonicalNativeIvEmbedPostBlock(
block,
result,
state,
depthContext);
case RichPageBlockKind::GroupedMedia:
return PrepareCanonicalNativeIvGroupedMediaBlock(
block,
result,
state,
path);
case RichPageBlockKind::Channel:
return PrepareNativeIvChannelBlock(block, result, state);
case RichPageBlockKind::Audio:
return PrepareCanonicalNativeIvMediaBlock(
block,
result,
state,
path,
PrepareNativeIvAudioBlock);
case RichPageBlockKind::Math:
if (block.formula.trimmed().isEmpty() && !state->editMode) {
return true;
} else {
auto prepared = PreparedBlock();
prepared.kind = PreparedBlockKind::DisplayMath;
prepared.formulaTex = block.formula;
prepared.mathKind = MathKind::Display;
prepared.editBlock = BlockSource(path);
prepared.editLeaf = MathFormulaLeafSource(path);
if (state->editMode) {
ApplyNativeIvEditPlaceholderText(&prepared);
}
prepared.formulaIndex = state->rememberFormula(prepared);
result->push_back(std::move(prepared));
return true;
}
case RichPageBlockKind::Table:
return PrepareCanonicalNativeIvTableBlock(block, result, state, path);
case RichPageBlockKind::Details:
return PrepareCanonicalNativeIvDetailsBlock(
block,
result,
state,
path,
depthContext);
case RichPageBlockKind::RelatedArticles:
return PrepareCanonicalNativeIvRelatedArticlesBlock(block, result, state);
case RichPageBlockKind::Map:
return PrepareCanonicalNativeIvMediaBlock(
block,
result,
state,
path,
PrepareNativeIvMapBlock);
}
return true;
}
[[nodiscard]] bool PrepareCanonicalNativeIvBlocks(
const std::vector<RichPageBlock> &blocks,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state,
PreparedEditBlockContainerPath container,
NativeIvDepthContext depthContext) {
for (auto i = 0, count = int(blocks.size()); i != count; ++i) {
const auto path = PreparedEditBlockPath{
.container = container,
.index = i,
};
if (!PrepareCanonicalNativeIvBlock(
blocks[i],
result,
state,
path,
depthContext)) {
if (state->result.failure.failed()) {
return false;
}
(void)PrepareNativeIvPlainPlaceholderBlock(
u"Unsupported Block"_q,
result);
}
}
return !state->result.failure.failed();
}
} // namespace
bool PrepareNativeIvBlocks(
const Iv::RichPage &page,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
return PrepareCanonicalNativeIvBlocks(
page.blocks,
result,
state,
PreparedEditBlockContainerPath(),
NativeIvDepthContext());
}
NativeInstantViewLeafUpdateResult UpdatePreparedNativeIvLeaf(
std::vector<PreparedBlock> *blocks,
const Iv::RichPage &page,
const PreparedEditLeafSource &source,
NativeIvPrepareState *state,
NativeIvPreparedLeafFormulaRange *formulaRange) {
if (!blocks || !state || !formulaRange) {
return NativeInstantViewLeafUpdateResult::Failed;
}
const auto canonicalBlock = ResolveCanonicalNativeIvBlock(page, source.block);
const auto preparedBlock = FindPreparedNativeIvBlockByPath(blocks, source.block);
if (!canonicalBlock || !preparedBlock) {
return NativeInstantViewLeafUpdateResult::Unsupported;
}
switch (source.kind) {
case PreparedEditLeafKind::BlockText:
case PreparedEditLeafKind::MathFormula:
case PreparedEditLeafKind::BlockCaption:
break;
case PreparedEditLeafKind::ListItemText:
if (source.listItemIndex < 0
|| source.listItemIndex >= int(canonicalBlock->listItems.size())) {
return NativeInstantViewLeafUpdateResult::Unsupported;
}
break;
case PreparedEditLeafKind::TableCellText:
if (source.tableRowIndex < 0
|| source.tableRowIndex >= int(canonicalBlock->tableRows.size())) {
return NativeInstantViewLeafUpdateResult::Unsupported;
}
if (source.tableCellIndex < 0
|| source.tableCellIndex
>= int(canonicalBlock->tableRows[source.tableRowIndex].cells.size())) {
return NativeInstantViewLeafUpdateResult::Unsupported;
}
break;
}
switch (source.kind) {
case PreparedEditLeafKind::BlockText:
case PreparedEditLeafKind::MathFormula:
return UpdatePreparedNativeIvBlockText(
preparedBlock,
*canonicalBlock,
source,
state,
formulaRange);
case PreparedEditLeafKind::BlockCaption:
return UpdatePreparedNativeIvBlockCaption(
preparedBlock,
*canonicalBlock,
source,
state,
formulaRange);
case PreparedEditLeafKind::ListItemText:
return UpdatePreparedNativeIvListItemText(
preparedBlock,
canonicalBlock->listItems[source.listItemIndex],
source,
state,
formulaRange);
case PreparedEditLeafKind::TableCellText:
return UpdatePreparedNativeIvTableCellText(
preparedBlock,
canonicalBlock->tableRows[source.tableRowIndex]
.cells[source.tableCellIndex],
source,
state,
formulaRange);
}
return NativeInstantViewLeafUpdateResult::Unsupported;
}
} // namespace Iv::Markdown