Files
tdesktop/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_blocks.cpp
2026-05-19 12:49:08 +04:00

1608 lines
44 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/openssl_help.h"
#include "base/unixtime.h"
#include "iv/iv_data.h"
#include "iv/markdown/iv_markdown_prepare_links.h"
#include "lang/lang_keys.h"
#include <algorithm>
#include <array>
#include <limits>
#include <utility>
namespace Iv::Markdown {
namespace {
void ShiftEntities(EntitiesInText *entities, int delta) {
if (!delta) {
return;
}
for (auto &entity : *entities) {
entity = EntityInText(
entity.type(),
entity.offset() + delta,
entity.length(),
entity.data());
}
}
void PrependText(TextWithEntities *text, QString prefix) {
if (prefix.isEmpty()) {
return;
}
text->text.prepend(prefix);
ShiftEntities(&text->entities, prefix.size());
}
[[nodiscard]] QString NativeIvDateText(TimeId date) {
return langDateTimeFull(base::unixtime::parse(date));
}
[[nodiscard]] QString NativeIvRelatedArticleFooterText(
const MTPDpageRelatedArticle &data) {
const auto author = qs(data.vauthor().value_or_empty()).trimmed();
const auto published = data.vpublished_date();
if (published && !author.isEmpty()) {
return author + u", "_q + NativeIvDateText(published->v);
} else if (published) {
return NativeIvDateText(published->v);
}
return author;
}
[[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]] uint16 InternalPreparedLinkIndex(const QString &data) {
const auto prefix = u"internal:index"_q;
return (data.size() == prefix.size() + 1 && data.startsWith(prefix))
? uint16(data.back().unicode())
: uint16(0);
}
[[nodiscard]] uint16 RemappedPreparedLinkIndex(
const std::vector<std::pair<uint16, uint16>> &remapped,
uint16 index) {
for (const auto &[from, to] : remapped) {
if (from == index) {
return to;
}
}
return 0;
}
void AppendPreparedIvRichText(
PreparedIvRichText *result,
PreparedIvRichText prepared) {
auto remapped = std::vector<std::pair<uint16, uint16>>();
remapped.reserve(prepared.links.size());
result->links.reserve(result->links.size() + prepared.links.size());
for (const auto &link : prepared.links) {
const auto index = result->links.size() + 1;
if (index > std::numeric_limits<uint16>::max()) {
continue;
}
auto copy = link;
copy.index = uint16(index);
remapped.push_back({ link.index, copy.index });
result->links.push_back(std::move(copy));
}
const auto shift = result->text.text.size();
result->text.text.append(prepared.text.text);
result->text.entities.reserve(
result->text.entities.size() + prepared.text.entities.size());
for (const auto &entity : prepared.text.entities) {
auto data = entity.data();
if (entity.type() == EntityType::CustomUrl) {
if (const auto from = InternalPreparedLinkIndex(data)) {
if (const auto to = RemappedPreparedLinkIndex(remapped, from)) {
data = InternalLinkData(to);
}
}
}
result->text.entities.push_back(EntityInText(
entity.type(),
entity.offset() + shift,
entity.length(),
data));
}
}
[[nodiscard]] bool PrepareNativeIvCaption(
const MTPPageCaption &caption,
PreparedIvRichText *result,
QString *blockAnchorId,
NativeIvPrepareState *state) {
auto text = PreparedIvRichText();
if (!PrepareNativeIvRichText(
caption.data().vtext(),
&text,
blockAnchorId,
state)) {
return false;
}
AppendPreparedIvRichText(result, std::move(text));
auto credit = PreparedIvRichText();
if (!PrepareNativeIvRichText(
caption.data().vcredit(),
&credit,
blockAnchorId,
state)) {
return false;
}
if (!credit.text.text.isEmpty()) {
if (!result->text.text.isEmpty()) {
result->text.append(QChar('\n'));
}
AppendPreparedIvRichText(result, std::move(credit));
}
return true;
}
struct NativeIvHtmlAttribute {
QByteArray name;
QByteArray value;
};
using NativeIvHtmlAttributes = std::vector<NativeIvHtmlAttribute>;
[[nodiscard]] QByteArray NativeIvHtmlEscape(QString text) {
return text.toHtmlEscaped().toUtf8();
}
[[nodiscard]] QByteArray NativeIvRichText(QString text) {
auto result = NativeIvHtmlEscape(std::move(text));
result.replace("\xE2\x81\xA6", "<span dir=\"ltr\">");
result.replace("\xE2\x81\xA7", "<span dir=\"rtl\">");
result.replace("\xE2\x81\xA8", "<span dir=\"auto\">");
result.replace("\xE2\x81\xA9", "</span>");
return result;
}
[[nodiscard]] QByteArray NativeIvHtmlTag(
const QByteArray &name,
const NativeIvHtmlAttributes &attributes = {},
const QByteArray &body = {}) {
auto serialized = QByteArray();
for (const auto &[attributeName, value] : attributes) {
if (attributeName.isEmpty()) {
continue;
}
serialized += ' ';
serialized += attributeName;
if (!value.isEmpty()) {
serialized += "=\"";
serialized += value;
serialized += '"';
}
}
return QByteArray("<")
+ name
+ serialized
+ '>'
+ body
+ "</"
+ name
+ '>';
}
[[nodiscard]] QByteArray NativeIvHashBytes(const QByteArray &bytes) {
auto binary = std::array<uchar, SHA256_DIGEST_LENGTH>{};
SHA256(
reinterpret_cast<const unsigned char*>(bytes.constData()),
bytes.size(),
binary.data());
auto result = QByteArray();
result.reserve(binary.size() * 2);
for (const auto byte : binary) {
const auto hex = [](uchar value) -> char {
return (value >= 10) ? ('a' + (value - 10)) : ('0' + value);
};
result.push_back(hex(byte / 16));
result.push_back(hex(byte % 16));
}
return result;
}
[[nodiscard]] QByteArray StoreNativeIvEmbedHtml(
QByteArray html,
NativeIvPrepareState *state) {
const auto resourceId = QByteArray("native-iv-embed/")
+ NativeIvHashBytes(html)
+ ".html";
state->result.embedHtmlResources.emplace(resourceId, std::move(html));
return resourceId;
}
[[nodiscard]] QByteArray NativeIvResourceUrl(QByteArray resourceId) {
return QByteArray("/") + resourceId;
}
[[nodiscard]] QByteArray NativeIvDocumentUrl(uint64 documentId) {
return NativeIvResourceUrl(
QByteArray("document/") + QByteArray::number(documentId));
}
[[nodiscard]] QByteArray WrapNativeIvEmbedHtml(QByteArray body) {
return QByteArray(
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
"<style>"
"html,body{margin:0;padding:0;background:#fff;color:#000;}"
"body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;}"
"figure{display:block;margin:0;max-width:100%;}"
"img,video{display:block;max-width:100%;height:auto;}"
".iframe-wrap{display:block;margin:0 auto;max-width:100%;overflow:hidden;}"
"iframe{display:block;width:100%;max-width:100%;border:0;}"
"</style></head><body>")
+ body
+ QByteArray(
"<script>"
"(function(){"
"var iframeWindows=new Map();"
"var lastPreferredWidth=0;"
"var lastPreferredHeight=0;"
"var measureScheduled=false;"
"function clamp(value,max){"
"value=Number(value);"
"if(!isFinite(value)){return 0;}"
"value=Math.round(value);"
"if(value<1){return 0;}"
"return Math.min(value,max||100000);"
"}"
"function findWrap(iframe){"
"var node=iframe;"
"while(node&&node!==document.body){"
"if(node.classList&&node.classList.contains('iframe-wrap')){"
"return node;"
"}"
"node=node.parentNode;"
"}"
"return null;"
"}"
"function rememberIframe(iframe){"
"try{"
"if(iframe.contentWindow){"
"iframeWindows.set(iframe.contentWindow,iframe);"
"}"
"}catch(e){"
"}"
"}"
"function seedIframeHeight(iframe){"
"var wrap=findWrap(iframe);"
"if(!wrap){return;}"
"var fixed=clamp(wrap.getAttribute('data-height'),100000);"
"if(fixed){"
"iframe.style.height=fixed+'px';"
"return;"
"}"
"if(iframe.getAttribute('data-native-iv-resized')==='1'){"
"return;"
"}"
"var ratio=Number(wrap.getAttribute('data-aspect-ratio'));"
"if(!isFinite(ratio)||ratio<=0){"
"return;"
"}"
"var width=clamp(Math.ceil("
"wrap.getBoundingClientRect().width"
"||iframe.getBoundingClientRect().width"
"||0),100000);"
"if(!width){"
"return;"
"}"
"var height=clamp(width*ratio,100000);"
"if(height){"
"iframe.style.height=height+'px';"
"}"
"}"
"function syncIframe(iframe){"
"rememberIframe(iframe);"
"seedIframeHeight(iframe);"
"if(iframe.getAttribute('data-native-iv-registered')==='1'){"
"return;"
"}"
"iframe.setAttribute('data-native-iv-registered','1');"
"iframe.addEventListener('load',function(){"
"rememberIframe(iframe);"
"seedIframeHeight(iframe);"
"scheduleMeasure();"
"},false);"
"}"
"function syncIframes(){"
"var iframes=document.getElementsByTagName('iframe');"
"for(var i=0;i<iframes.length;i++){"
"syncIframe(iframes[i]);"
"}"
"}"
"function measurePreferredSize(){"
"var width=0;"
"var height=0;"
"var body=document.body;"
"if(body){"
"var bodyRect=body.getBoundingClientRect();"
"height=Math.max(height,body.scrollHeight,body.offsetHeight,bodyRect.height);"
"var children=body.children;"
"for(var i=0;i<children.length;i++){"
"var child=children[i];"
"var rect=child.getBoundingClientRect();"
"width=Math.max(width,child.scrollWidth,child.offsetWidth,rect.width);"
"height=Math.max(height,child.scrollHeight,child.offsetHeight,rect.bottom-bodyRect.top);"
"}"
"if(!width){"
"width=Math.max(body.scrollWidth,body.offsetWidth,bodyRect.width);"
"}"
"}"
"var doc=document.documentElement;"
"if(doc&&(!width||!height)){"
"var docRect=doc.getBoundingClientRect();"
"if(!width){"
"width=Math.max(doc.scrollWidth,doc.offsetWidth,doc.clientWidth,docRect.width);"
"}"
"if(!height){"
"height=Math.max(doc.scrollHeight,doc.offsetHeight,doc.clientHeight,docRect.height);"
"}"
"}"
"return{"
"width:clamp(width,100000),"
"height:clamp(height,100000)"
"};"
"}"
"function reportPreferredSize(){"
"var size=measurePreferredSize();"
"if(!size.width||!size.height){"
"return;"
"}"
"if(size.width===lastPreferredWidth&&size.height===lastPreferredHeight){"
"return;"
"}"
"lastPreferredWidth=size.width;"
"lastPreferredHeight=size.height;"
"if(window.external&&typeof window.external.invoke==='function'){"
"try{"
"window.external.invoke(JSON.stringify({"
"event:'preferred_size',"
"width:size.width,"
"height:size.height"
"}));"
"}catch(e){"
"}"
"}"
"}"
"function scheduleMeasure(){"
"if(measureScheduled){"
"return;"
"}"
"measureScheduled=true;"
"var callback=function(){"
"measureScheduled=false;"
"reportPreferredSize();"
"};"
"if(window.requestAnimationFrame){"
"window.requestAnimationFrame(callback);"
"}else{"
"setTimeout(callback,0);"
"}"
"}"
"window.addEventListener('message',function(event){"
"var iframe=iframeWindows.get(event.source);"
"if(!iframe){"
"return;"
"}"
"var data=event.data;"
"if(typeof data==='string'){"
"try{"
"data=JSON.parse(data);"
"}catch(e){"
"return;"
"}"
"}"
"if(!data||data.eventType!=='resize_frame'||!data.eventData){"
"return;"
"}"
"var height=clamp(data.eventData.height,100000);"
"if(!height){"
"return;"
"}"
"iframe.setAttribute('data-native-iv-resized','1');"
"iframe.style.height=height+'px';"
"scheduleMeasure();"
"},false);"
"document.addEventListener('DOMContentLoaded',function(){"
"syncIframes();"
"scheduleMeasure();"
"},false);"
"window.addEventListener('load',function(){"
"syncIframes();"
"scheduleMeasure();"
"},false);"
"window.addEventListener('resize',function(){"
"syncIframes();"
"scheduleMeasure();"
"},false);"
"if(window.ResizeObserver){"
"var observer=new ResizeObserver(function(){"
"scheduleMeasure();"
"});"
"if(document.documentElement){"
"observer.observe(document.documentElement);"
"}"
"if(document.body){"
"observer.observe(document.body);"
"}"
"}else{"
"setInterval(scheduleMeasure,250);"
"}"
"syncIframes();"
"scheduleMeasure();"
"})();"
"</script></body></html>");
}
[[nodiscard]] const NativeIvPhotoInfo *FindNativeIvPhoto(
uint64 photoId,
const NativeIvPrepareState &state) {
for (const auto &photo : state.photos) {
if (photo.id == photoId) {
return &photo;
}
}
return nullptr;
}
[[nodiscard]] const NativeIvDocumentInfo *FindNativeIvDocument(
uint64 documentId,
const NativeIvPrepareState &state) {
for (const auto &document : state.documents) {
if (document.id == documentId) {
return &document;
}
}
return nullptr;
}
[[nodiscard]] QByteArray RenderNativeIvRichTextHtml(
const MTPRichText &text,
NativeIvPrepareState *state) {
return text.match([&](const MTPDtextEmpty &) {
return QByteArray();
}, [&](const MTPDtextPlain &data) {
return NativeIvRichText(qs(data.vtext()));
}, [&](const MTPDtextConcat &data) {
auto result = QByteArray();
for (const auto &part : data.vtexts().v) {
result += RenderNativeIvRichTextHtml(part, state);
}
return result;
}, [&](const MTPDtextImage &data) {
const auto documentId = uint64(data.vdocument_id().v);
if (!FindNativeIvDocument(documentId, *state)) {
return NativeIvRichText(u"Image not found."_q);
}
auto attributes = NativeIvHtmlAttributes{
{ "class", "pic" },
{ "src", NativeIvHtmlEscape(
QString::fromUtf8(NativeIvDocumentUrl(documentId))) },
};
if (const auto width = data.vw().v) {
attributes.push_back({ "width", QByteArray::number(width) });
}
if (const auto height = data.vh().v) {
attributes.push_back({ "height", QByteArray::number(height) });
}
return NativeIvHtmlTag("img", attributes);
}, [&](const MTPDtextBold &data) {
return NativeIvHtmlTag("b", {}, RenderNativeIvRichTextHtml(data.vtext(), state));
}, [&](const MTPDtextItalic &data) {
return NativeIvHtmlTag("i", {}, RenderNativeIvRichTextHtml(data.vtext(), state));
}, [&](const MTPDtextUnderline &data) {
return NativeIvHtmlTag("u", {}, RenderNativeIvRichTextHtml(data.vtext(), state));
}, [&](const MTPDtextStrike &data) {
return NativeIvHtmlTag("s", {}, RenderNativeIvRichTextHtml(data.vtext(), state));
}, [&](const MTPDtextFixed &data) {
return NativeIvHtmlTag("code", {}, RenderNativeIvRichTextHtml(data.vtext(), state));
}, [&](const MTPDtextUrl &data) {
auto attributes = NativeIvHtmlAttributes{
{ "href", NativeIvHtmlEscape(qs(data.vurl())) },
};
return NativeIvHtmlTag(
"a",
attributes,
RenderNativeIvRichTextHtml(data.vtext(), state));
}, [&](const MTPDtextEmail &data) {
return NativeIvHtmlTag(
"a",
{ { "href", "mailto:" + NativeIvHtmlEscape(qs(data.vemail())) } },
RenderNativeIvRichTextHtml(data.vtext(), state));
}, [&](const MTPDtextSubscript &data) {
return NativeIvHtmlTag("sub", {}, RenderNativeIvRichTextHtml(data.vtext(), state));
}, [&](const MTPDtextSuperscript &data) {
return NativeIvHtmlTag("sup", {}, RenderNativeIvRichTextHtml(data.vtext(), state));
}, [&](const MTPDtextMarked &data) {
return NativeIvHtmlTag("mark", {}, RenderNativeIvRichTextHtml(data.vtext(), state));
}, [&](const MTPDtextPhone &data) {
return NativeIvHtmlTag(
"a",
{ { "href", "tel:" + NativeIvHtmlEscape(qs(data.vphone())) } },
RenderNativeIvRichTextHtml(data.vtext(), state));
}, [&](const MTPDtextAnchor &data) {
const auto inner = RenderNativeIvRichTextHtml(data.vtext(), state);
const auto name = NativeIvHtmlEscape(qs(data.vname()));
return inner.isEmpty()
? NativeIvHtmlTag("a", { { "name", name } })
: NativeIvHtmlTag(
"span",
{ { "class", "reference" } },
NativeIvHtmlTag("a", { { "name", name } }) + inner);
});
}
[[nodiscard]] QByteArray NativeIvCaptionHtml(
const MTPPageCaption &caption,
NativeIvPrepareState *state) {
auto text = RenderNativeIvRichTextHtml(caption.data().vtext(), state);
const auto credit = RenderNativeIvRichTextHtml(caption.data().vcredit(), state);
if (!credit.isEmpty()) {
text += NativeIvHtmlTag("cite", { { "dir", "auto" } }, credit);
} else if (text.isEmpty()) {
return QByteArray();
}
return NativeIvHtmlTag("figcaption", { { "dir", "auto" } }, text);
}
[[nodiscard]] QString StripOneTrailingNewline(QString text);
[[nodiscard]] PreparedBlock PrepareNativeIvEmbedPostFallbackParagraph(
QString url);
[[nodiscard]] QByteArray RenderNativeIvEmbedHtml(
const MTPDpageBlockEmbed &data,
NativeIvPrepareState *state,
bool includeCaption) {
auto iframeWidth = QByteArray();
auto iframeHeight = QByteArray();
auto width = QByteArray();
auto height = QByteArray();
auto aspectRatio = QByteArray();
auto iframeAttributes = NativeIvHtmlAttributes();
const auto autosize = !data.vw();
if (autosize) {
iframeWidth = "100%";
} else if (data.is_full_width() || !data.vw()->v) {
width = "100%";
height = QByteArray::number(data.vh()->v) + "px";
iframeWidth = "100%";
iframeHeight = height;
} else {
width = QByteArray::number(data.vw()->v) + "px";
aspectRatio = QByteArray::number(
double(data.vh()->v) / std::max(data.vw()->v, 1),
'g',
16);
}
if (!iframeWidth.isEmpty()) {
iframeAttributes.push_back({ "width", iframeWidth });
}
if (!iframeHeight.isEmpty()) {
iframeAttributes.push_back({ "height", iframeHeight });
}
if (const auto url = data.vurl()) {
if (autosize) {
iframeAttributes.push_back({
"srcdoc",
NativeIvHtmlEscape(qs(*url)),
});
} else {
iframeAttributes.push_back({
"src",
NativeIvHtmlEscape(qs(*url)),
});
}
} else if (const auto html = data.vhtml()) {
const auto resourceId = StoreNativeIvEmbedHtml(html->v, state);
iframeAttributes.push_back({
"src",
NativeIvHtmlEscape(QString::fromUtf8(NativeIvResourceUrl(resourceId))),
});
} else {
return QByteArray();
}
if (!data.is_allow_scrolling()) {
iframeAttributes.push_back({ "scrolling", "no" });
}
iframeAttributes.push_back({ "frameborder", "0" });
iframeAttributes.push_back({ "allowtransparency", "true" });
iframeAttributes.push_back({ "allowfullscreen", "true" });
auto content = NativeIvHtmlTag("iframe", iframeAttributes);
if (!autosize) {
auto wrapAttributes = NativeIvHtmlAttributes{
{ "class", "iframe-wrap" },
};
auto style = QByteArray();
if (!width.isEmpty()) {
style += QByteArray("width:") + width + ';';
}
if (!style.isEmpty()) {
wrapAttributes.push_back({ "style", style });
}
if (!height.isEmpty()) {
wrapAttributes.push_back({
"data-height",
QByteArray::number(data.vh()->v),
});
} else if (!aspectRatio.isEmpty()) {
wrapAttributes.push_back({
"data-aspect-ratio",
aspectRatio,
});
}
content = NativeIvHtmlTag("div", wrapAttributes, content);
}
if (includeCaption) {
content += NativeIvCaptionHtml(data.vcaption(), state);
}
auto figureAttributes = NativeIvHtmlAttributes();
if (!data.is_full_width()) {
figureAttributes.push_back({ "class", "nowide" });
}
return NativeIvHtmlTag("figure", figureAttributes, content);
}
[[nodiscard]] bool PrepareNativeIvEmbedBlock(
const MTPDpageBlockEmbed &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
const auto html = RenderNativeIvEmbedHtml(data, state, false);
if (html.isEmpty()) {
return PrepareNativeIvPlaceholderBlock(
u"Embed Placeholder"_q,
data.vcaption(),
result,
state);
}
auto request = EmbedRequest{
.resourceId = StoreNativeIvEmbedHtml(
WrapNativeIvEmbedHtml(html),
state),
.fallbackUrl = (!data.vw() || !data.vurl())
? QString()
: qs(*data.vurl()),
.width = data.vw() ? data.vw()->v : 0,
.height = data.vh() ? data.vh()->v : 0,
.fullWidth = data.is_full_width(),
.allowScrolling = data.is_allow_scrolling(),
};
return PrepareNativeIvPlaceholderBlock(
u"Embed Placeholder"_q,
data.vcaption(),
result,
state,
std::move(request));
}
[[nodiscard]] bool PrepareNativeIvEmbedPostBlock(
const MTPDpageBlockEmbedPost &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
auto caption = PreparedIvRichText();
auto block = PreparedBlock();
block.kind = PreparedBlockKind::EmbedPost;
block.embedPost.url = qs(data.vurl()).trimmed();
block.embedPost.authorPhotoId = uint64(data.vauthor_photo_id().v);
block.embedPost.author = qs(data.vauthor()).trimmed();
if (const auto date = data.vdate().v) {
block.embedPost.dateText = NativeIvDateText(date);
}
if (!PrepareNativeIvCaption(
data.vcaption(),
&caption,
&block.anchorId,
state)) {
return false;
}
SortPreparedIvRichText(&caption);
block.text = std::move(caption.text);
block.links = std::move(caption.links);
block.supplementary = true;
if (data.vblocks().v.isEmpty()) {
block.children.push_back(
PrepareNativeIvEmbedPostFallbackParagraph(block.embedPost.url));
} else if (!PrepareNativeIvBlocks(
data.vblocks().v,
&block.children,
state)) {
return false;
}
result->push_back(std::move(block));
return true;
}
[[nodiscard]] QString StripOneTrailingNewline(QString text) {
if (text.endsWith(u"\r\n"_q)) {
text.chop(2);
} else if (!text.isEmpty()) {
const auto last = text.back();
if ((last == QChar(u'\n')) || (last == QChar(u'\r'))) {
text.chop(1);
}
}
return text;
}
[[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;
}
[[nodiscard]] bool AppendNativeIvFlowBlock(
std::vector<PreparedBlock> *result,
PreparedBlockKind kind,
int headingLevel,
const MTPRichText &text,
NativeIvPrepareState *state,
bool allowEmpty = false) {
auto prepared = PreparedIvRichText();
auto anchorId = QString();
if (!PrepareNativeIvRichText(text, &prepared, &anchorId, state)) {
return false;
}
return AppendPreparedIvRichBlock(
result,
kind,
headingLevel,
std::move(prepared),
std::move(anchorId),
allowEmpty);
}
[[nodiscard]] bool PrepareNativeIvQuoteBlock(
const MTPRichText &text,
const MTPRichText &caption,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Quote;
auto body = PreparedIvRichText();
if (!PrepareNativeIvRichText(text, &body, &block.anchorId, state)) {
return false;
}
if (!AppendPreparedIvRichBlock(
&block.children,
PreparedBlockKind::Paragraph,
0,
std::move(body))) {
return false;
}
auto cite = PreparedIvRichText();
if (!PrepareNativeIvRichText(caption, &cite, &block.anchorId, state)) {
return false;
}
if (!AppendPreparedIvRichBlock(
&block.children,
PreparedBlockKind::Paragraph,
0,
std::move(cite),
QString(),
false,
true)) {
return false;
}
if (block.children.empty()) {
block.children.push_back(EmptyParagraphBlock());
}
result->push_back(std::move(block));
return true;
}
[[nodiscard]] bool PrepareNativeIvList(
const QVector<MTPPageListItem> &items,
PreparedBlock *result,
NativeIvPrepareState *state) {
for (const auto &item : items) {
auto block = PreparedBlock();
block.kind = PreparedBlockKind::ListItem;
block.listKind = result->listKind;
block.listDelimiter = result->listDelimiter;
const auto ok = item.match([&](const MTPDpageListItemText &data) {
auto prepared = PreparedIvRichText();
if (!PrepareNativeIvRichText(
data.vtext(),
&prepared,
&block.anchorId,
state)) {
return false;
}
return AppendPreparedIvRichBlock(
&block.children,
PreparedBlockKind::Paragraph,
0,
std::move(prepared));
}, [&](const MTPDpageListItemBlocks &data) {
return PrepareNativeIvBlocks(data.vblocks().v, &block.children, state);
});
if (!ok) {
return false;
}
if (block.children.empty()) {
block.children.push_back(EmptyParagraphBlock());
}
result->children.push_back(std::move(block));
}
return true;
}
[[nodiscard]] bool ParseOrderedNumber(
const QString &value,
int *result) {
auto ok = false;
const auto parsed = value.toInt(&ok);
if (!ok) {
return false;
}
*result = parsed;
return true;
}
[[nodiscard]] int NextNativeIvOrderedNumber(const PreparedBlock &result) {
return result.children.empty()
? result.startNumber
: (result.children.back().orderedNumber + 1);
}
[[nodiscard]] bool PrepareNativeIvOrderedList(
const QVector<MTPPageListOrderedItem> &items,
PreparedBlock *result,
NativeIvPrepareState *state) {
auto firstNumber = true;
for (const auto &item : items) {
auto block = PreparedBlock();
block.kind = PreparedBlockKind::ListItem;
block.listKind = result->listKind;
block.listDelimiter = result->listDelimiter;
const auto ok = item.match([&](const MTPDpageListOrderedItemText &data) {
auto orderedNumber = 0;
if (!ParseOrderedNumber(qs(data.vnum()), &orderedNumber)) {
orderedNumber = NextNativeIvOrderedNumber(*result);
}
block.orderedNumber = orderedNumber;
auto prepared = PreparedIvRichText();
if (!PrepareNativeIvRichText(
data.vtext(),
&prepared,
&block.anchorId,
state)) {
return false;
}
return AppendPreparedIvRichBlock(
&block.children,
PreparedBlockKind::Paragraph,
0,
std::move(prepared));
}, [&](const MTPDpageListOrderedItemBlocks &data) {
auto orderedNumber = 0;
if (!ParseOrderedNumber(qs(data.vnum()), &orderedNumber)) {
orderedNumber = NextNativeIvOrderedNumber(*result);
}
block.orderedNumber = orderedNumber;
return PrepareNativeIvBlocks(data.vblocks().v, &block.children, state);
});
if (!ok) {
return false;
}
if (firstNumber) {
result->startNumber = block.orderedNumber;
firstNumber = false;
}
if (block.children.empty()) {
block.children.push_back(EmptyParagraphBlock());
}
result->children.push_back(std::move(block));
}
return true;
}
[[nodiscard]] TableAlignment NativeIvTableAlignment(
const MTPDpageTableCell &cell) {
if (cell.is_align_right()) {
return TableAlignment::Right;
} else if (cell.is_align_center()) {
return TableAlignment::Center;
}
return TableAlignment::Left;
}
[[nodiscard]] PreparedTableCellVerticalAlignment NativeIvTableVerticalAlignment(
const MTPDpageTableCell &cell) {
if (cell.is_valign_bottom()) {
return PreparedTableCellVerticalAlignment::Bottom;
} else if (cell.is_valign_middle()) {
return PreparedTableCellVerticalAlignment::Middle;
}
return PreparedTableCellVerticalAlignment::Top;
}
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;
}
[[nodiscard]] bool PrepareNativeIvTableBlock(
const MTPDpageBlockTable &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Table;
block.tableBordered = data.is_bordered();
block.tableStriped = data.is_striped();
auto title = PreparedIvRichText();
if (!PrepareNativeIvRichText(
data.vtitle(),
&title,
&block.anchorId,
state)) {
return false;
}
SortPreparedIvRichText(&title);
block.text = std::move(title.text);
block.links = std::move(title.links);
const auto &limits = PrepareTableRenderLimitsForIv();
const auto rowCount = std::min(int(data.vrows().v.size()), limits.maxRows);
auto occupancy = NativeIvTableOccupancyGrid(rowCount);
block.tableRows.reserve(rowCount);
auto occupiedSlotCountSoFar = int64(0);
auto rowIndex = 0;
for (const auto &row : data.vrows().v) {
if (rowIndex >= rowCount) {
break;
}
auto preparedRow = PreparedTableRow();
const auto ok = row.match([&](const MTPDpageTableRow &rowData) {
preparedRow.cells.reserve(std::min(
int(rowData.vcells().v.size()),
limits.maxColumns));
for (const auto &cell : rowData.vcells().v) {
auto preparedCell = PreparedTableCell();
const auto cellOk = cell.match([&](const MTPDpageTableCell &cellData) {
const auto rawColspan = cellData.vcolspan()
? cellData.vcolspan()->v
: 1;
const auto rawRowspan = cellData.vrowspan()
? cellData.vrowspan()->v
: 1;
const auto normalizedColspan = NormalizeNativeIvTableSpan(
rawColspan);
const auto rowspan = ClampNativeIvTableRowspan(
rawRowspan,
rowIndex,
rowCount);
if (rowspan <= 0) {
return true;
}
const auto column = FirstAvailableNativeIvTableColumn(
occupancy,
rowIndex,
rowspan,
normalizedColspan,
limits.maxColumns);
if (column < 0) {
return true;
}
const auto colspan = ClampNativeIvTableColspan(
normalizedColspan,
column,
limits.maxColumns);
if (colspan <= 0) {
return true;
}
const auto occupiedSlotGrowth = int64(rowspan) * colspan;
if (occupiedSlotGrowth > limits.maxCells
|| (occupiedSlotCountSoFar + occupiedSlotGrowth)
> limits.maxCells) {
return true;
}
preparedCell.column = column;
preparedCell.alignment = NativeIvTableAlignment(cellData);
preparedCell.header = cellData.is_header();
preparedCell.verticalAlignment
= NativeIvTableVerticalAlignment(cellData);
preparedCell.colspan = colspan;
preparedCell.rowspan = rowspan;
if (cellData.vtext()) {
auto rich = PreparedIvRichText();
if (!PrepareNativeIvRichText(
*cellData.vtext(),
&rich,
nullptr,
state)) {
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));
return true;
});
if (!cellOk) {
return false;
}
}
preparedRow.header = !preparedRow.cells.empty()
&& std::all_of(
preparedRow.cells.begin(),
preparedRow.cells.end(),
[](const PreparedTableCell &cell) {
return cell.header;
});
return true;
});
if (!ok) {
return false;
}
block.tableRows.push_back(std::move(preparedRow));
++rowIndex;
}
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 PrepareNativeIvDetailsBlock(
const MTPDpageBlockDetails &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
auto summary = PreparedIvRichText();
auto anchorId = NativeIvDetailsAnchorId(state);
if (!PrepareNativeIvRichText(
data.vtitle(),
&summary,
&anchorId,
state)) {
return false;
}
if (!summary.links.empty()) {
const auto isLink = [](const EntityInText &entity) {
return entity.type() == EntityType::CustomUrl;
};
const auto from = std::remove_if(
summary.text.entities.begin(),
summary.text.entities.end(),
isLink);
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.collapsed = !data.is_open();
block.text = std::move(summary.text);
block.links = std::move(summary.links);
if (!PrepareNativeIvBlocks(data.vblocks().v, &block.children, state)) {
return false;
}
result->push_back(std::move(block));
return true;
}
[[nodiscard]] bool PrepareNativeIvRelatedArticlesBlock(
const MTPDpageBlockRelatedArticles &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
auto related = std::vector<PreparedBlock>();
related.reserve(data.varticles().v.size());
for (const auto &article : data.varticles().v) {
auto prepared = PreparedBlock();
prepared.kind = PreparedBlockKind::RelatedArticle;
const auto &row = article.data();
prepared.relatedArticle.title = qs(
row.vtitle().value_or_empty()).trimmed();
prepared.relatedArticle.description = qs(
row.vdescription().value_or_empty()).trimmed();
prepared.relatedArticle.footer = NativeIvRelatedArticleFooterText(row);
prepared.relatedArticle.photoId = row.vphoto_id().value_or_empty();
if (prepared.relatedArticle.title.isEmpty()
&& prepared.relatedArticle.description.isEmpty()
&& prepared.relatedArticle.footer.isEmpty()) {
prepared.relatedArticle.title = qs(row.vurl()).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(
qs(row.vurl()),
uint64(row.vwebpage_id().v),
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 = QString();
if (!PrepareNativeIvRichText(
data.vtitle(),
&title,
&anchorId,
state)) {
return false;
}
SortPreparedIvRichText(&title);
if (!title.text.text.isEmpty()) {
if (!AppendPreparedIvRichBlock(
result,
PreparedBlockKind::Heading,
4,
std::move(title),
std::move(anchorId))) {
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 PrepareNativeIvBlock(
const MTPPageBlock &block,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state);
[[nodiscard]] bool PrepareNativeIvBlock(
const MTPPageBlock &block,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
if (state->blocked()) {
return false;
}
return block.match([&](const MTPDpageBlockUnsupported &) {
return PrepareNativeIvPlainPlaceholderBlock(
u"Unsupported Content"_q,
result);
}, [&](const MTPDpageBlockTitle &data) {
return AppendNativeIvFlowBlock(
result,
PreparedBlockKind::Heading,
1,
data.vtext(),
state);
}, [&](const MTPDpageBlockSubtitle &data) {
return AppendNativeIvFlowBlock(
result,
PreparedBlockKind::Heading,
2,
data.vtext(),
state);
}, [&](const MTPDpageBlockAuthorDate &data) {
auto prepared = PreparedIvRichText();
auto anchorId = QString();
if (!PrepareNativeIvRichText(
data.vauthor(),
&prepared,
&anchorId,
state)) {
return false;
}
if (const auto date = data.vpublished_date().v) {
if (!prepared.text.text.isEmpty()) {
prepared.text.append(u" \u2022 "_q);
}
prepared.text.append(NativeIvDateText(date));
}
return AppendPreparedIvRichBlock(
result,
PreparedBlockKind::Paragraph,
0,
std::move(prepared),
std::move(anchorId),
false,
true);
}, [&](const MTPDpageBlockHeader &data) {
return AppendNativeIvFlowBlock(
result,
PreparedBlockKind::Heading,
3,
data.vtext(),
state);
}, [&](const MTPDpageBlockSubheader &data) {
return AppendNativeIvFlowBlock(
result,
PreparedBlockKind::Heading,
4,
data.vtext(),
state);
}, [&](const MTPDpageBlockParagraph &data) {
return AppendNativeIvFlowBlock(
result,
PreparedBlockKind::Paragraph,
0,
data.vtext(),
state);
}, [&](const MTPDpageBlockPreformatted &data) {
auto prepared = PreparedIvRichText();
auto anchorId = QString();
if (!PrepareNativeIvRichText(
data.vtext(),
&prepared,
&anchorId,
state)) {
return false;
}
auto block = PreparedBlock();
block.kind = PreparedBlockKind::CodeBlock;
block.anchorId = std::move(anchorId);
block.codeLanguage = qs(data.vlanguage()).trimmed();
block.text.text = StripOneTrailingNewline(prepared.text.text);
result->push_back(std::move(block));
return true;
}, [&](const MTPDpageBlockFooter &data) {
return AppendNativeIvFlowBlock(
result,
PreparedBlockKind::Paragraph,
0,
data.vtext(),
state);
}, [&](const MTPDpageBlockDivider &) {
result->push_back(PrepareRuleBlock());
return true;
}, [&](const MTPDpageBlockAnchor &data) {
const auto anchorId = NormalizeFragmentId(qs(data.vname()));
if (anchorId.isEmpty()) {
return true;
}
auto prepared = PreparedIvRichText();
return AppendPreparedIvRichBlock(
result,
PreparedBlockKind::Paragraph,
0,
std::move(prepared),
anchorId,
true);
}, [&](const MTPDpageBlockList &data) {
auto prepared = PreparedBlock();
prepared.kind = PreparedBlockKind::List;
prepared.listKind = ListKind::Bullet;
return PrepareNativeIvList(data.vitems().v, &prepared, state)
? (result->push_back(std::move(prepared)), true)
: false;
}, [&](const MTPDpageBlockBlockquote &data) {
return PrepareNativeIvQuoteBlock(
data.vtext(),
data.vcaption(),
result,
state);
}, [&](const MTPDpageBlockPullquote &data) {
return PrepareNativeIvQuoteBlock(
data.vtext(),
data.vcaption(),
result,
state);
}, [&](const MTPDpageBlockPhoto &data) {
return PrepareNativeIvPhotoBlock(data, result, state);
}, [&](const MTPDpageBlockVideo &data) {
return PrepareNativeIvVideoBlock(data, result, state);
}, [&](const MTPDpageBlockCover &data) {
return PrepareNativeIvBlock(data.vcover(), result, state);
}, [&](const MTPDpageBlockEmbed &data) {
return PrepareNativeIvEmbedBlock(data, result, state);
}, [&](const MTPDpageBlockEmbedPost &data) {
return PrepareNativeIvEmbedPostBlock(data, result, state);
}, [&](const MTPDpageBlockCollage &data) {
return PrepareNativeIvGroupedMediaBlock(
data.vitems().v,
data.vcaption(),
PreparedGroupedMediaIntent::Collage,
u"Collage placeholder"_q,
result,
state);
}, [&](const MTPDpageBlockSlideshow &data) {
return PrepareNativeIvGroupedMediaBlock(
data.vitems().v,
data.vcaption(),
PreparedGroupedMediaIntent::Slideshow,
u"Grouped Media Placeholder"_q,
result,
state);
}, [&](const MTPDpageBlockChannel &data) {
return PrepareNativeIvChannelBlock(data, result, state);
}, [&](const MTPDpageBlockAudio &data) {
return PrepareNativeIvAudioBlock(data, result, state);
}, [&](const MTPDpageBlockKicker &data) {
return AppendNativeIvFlowBlock(
result,
PreparedBlockKind::Heading,
5,
data.vtext(),
state);
}, [&](const MTPDpageBlockTable &data) {
return PrepareNativeIvTableBlock(data, result, state);
}, [&](const MTPDpageBlockOrderedList &data) {
auto prepared = PreparedBlock();
prepared.kind = PreparedBlockKind::List;
prepared.listKind = ListKind::Ordered;
prepared.listDelimiter = ListDelimiter::Period;
prepared.startNumber = 1;
return PrepareNativeIvOrderedList(data.vitems().v, &prepared, state)
? (result->push_back(std::move(prepared)), true)
: false;
}, [&](const MTPDpageBlockDetails &data) {
return PrepareNativeIvDetailsBlock(data, result, state);
}, [&](const MTPDpageBlockRelatedArticles &data) {
return PrepareNativeIvRelatedArticlesBlock(data, result, state);
}, [&](const MTPDpageBlockMap &data) {
return PrepareNativeIvMapBlock(data, result, state);
});
}
} // namespace
bool PrepareNativeIvBlocks(
const QVector<MTPPageBlock> &blocks,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
for (const auto &block : blocks) {
if (!PrepareNativeIvBlock(block, result, state)) {
if (state->result.failure.failed()) {
return false;
}
(void)PrepareNativeIvPlainPlaceholderBlock(
u"Unsupported Block"_q,
result);
}
}
return !state->result.failure.failed();
}
} // namespace Iv::Markdown