1270 lines
		
	
	
	
		
			37 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			1270 lines
		
	
	
	
		
			37 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/iv_prepare.h"
 | 
						|
 | 
						|
#include "base/openssl_help.h"
 | 
						|
#include "base/unixtime.h"
 | 
						|
#include "iv/iv_data.h"
 | 
						|
#include "lang/lang_keys.h"
 | 
						|
#include "ui/image/image_prepare.h"
 | 
						|
#include "ui/grouped_layout.h"
 | 
						|
#include "styles/palette.h"
 | 
						|
#include "styles/style_chat.h"
 | 
						|
 | 
						|
#include <QtCore/QSize>
 | 
						|
 | 
						|
namespace Iv {
 | 
						|
namespace {
 | 
						|
 | 
						|
struct Attribute {
 | 
						|
	QByteArray name;
 | 
						|
	std::optional<QByteArray> value;
 | 
						|
};
 | 
						|
using Attributes = std::vector<Attribute>;
 | 
						|
 | 
						|
struct Photo {
 | 
						|
	uint64 id = 0;
 | 
						|
	int width = 0;
 | 
						|
	int height = 0;
 | 
						|
	QByteArray minithumbnail;
 | 
						|
};
 | 
						|
 | 
						|
struct Document {
 | 
						|
	uint64 id = 0;
 | 
						|
	int width = 0;
 | 
						|
	int height = 0;
 | 
						|
	QByteArray minithumbnail;
 | 
						|
};
 | 
						|
 | 
						|
template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>
 | 
						|
[[nodiscard]] QByteArray Number(T value) {
 | 
						|
	return QByteArray::number(value);
 | 
						|
}
 | 
						|
 | 
						|
template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>
 | 
						|
[[nodiscard]] QByteArray Percent(T value) {
 | 
						|
	return Number(base::SafeRound(value * 10000.) / 100.);
 | 
						|
};
 | 
						|
 | 
						|
[[nodiscard]] QByteArray Escape(QByteArray value) {
 | 
						|
	auto result = QByteArray();
 | 
						|
	result.reserve(value.size());
 | 
						|
	for (const auto &ch : value) {
 | 
						|
		switch (ch) {
 | 
						|
		case '&': result.append("&"); break;
 | 
						|
		case '<': result.append("<"); break;
 | 
						|
		case '>': result.append(">"); break;
 | 
						|
		case '"': result.append("""); break;
 | 
						|
		case '\'': result.append("'"); break;
 | 
						|
		default: result.append(ch); break;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
[[nodiscard]] QByteArray Date(TimeId date) {
 | 
						|
	return Escape(langDateTimeFull(base::unixtime::parse(date)).toUtf8());
 | 
						|
}
 | 
						|
 | 
						|
class Parser final {
 | 
						|
public:
 | 
						|
	Parser(const Source &source, const Options &options);
 | 
						|
 | 
						|
	[[nodiscard]] Prepared result();
 | 
						|
 | 
						|
private:
 | 
						|
	void process(const Source &source);
 | 
						|
	void process(const MTPPhoto &photo);
 | 
						|
	void process(const MTPDocument &document);
 | 
						|
 | 
						|
	template <typename Inner>
 | 
						|
	[[nodiscard]] QByteArray list(const MTPVector<Inner> &data);
 | 
						|
 | 
						|
	[[nodiscard]] QByteArray collage(
 | 
						|
		const QVector<MTPPageBlock> &list,
 | 
						|
		const std::vector<QSize> &dimensions,
 | 
						|
		int offset = 0);
 | 
						|
	[[nodiscard]] QByteArray slideshow(
 | 
						|
		const QVector<MTPPageBlock> &list,
 | 
						|
		QSize dimensions);
 | 
						|
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockUnsupported &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockTitle &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockSubtitle &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockAuthorDate &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockHeader &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockSubheader &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockParagraph &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockPreformatted &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockFooter &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockDivider &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockAnchor &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockList &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockBlockquote &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockPullquote &data);
 | 
						|
	[[nodiscard]] QByteArray block(
 | 
						|
		const MTPDpageBlockPhoto &data,
 | 
						|
		const Ui::GroupMediaLayout &layout = {},
 | 
						|
		QSize outer = {});
 | 
						|
	[[nodiscard]] QByteArray block(
 | 
						|
		const MTPDpageBlockVideo &data,
 | 
						|
		const Ui::GroupMediaLayout &layout = {},
 | 
						|
		QSize outer = {});
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockCover &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockEmbed &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockEmbedPost &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockCollage &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockSlideshow &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockChannel &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockAudio &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockKicker &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockTable &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockOrderedList &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockDetails &data);
 | 
						|
	[[nodiscard]] QByteArray block(
 | 
						|
		const MTPDpageBlockRelatedArticles &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageBlockMap &data);
 | 
						|
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageRelatedArticle &data);
 | 
						|
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageTableRow &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageTableCell &data);
 | 
						|
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageListItemText &data);
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageListItemBlocks &data);
 | 
						|
 | 
						|
	[[nodiscard]] QByteArray block(const MTPDpageListOrderedItemText &data);
 | 
						|
	[[nodiscard]] QByteArray block(
 | 
						|
		const MTPDpageListOrderedItemBlocks &data);
 | 
						|
 | 
						|
	[[nodiscard]] QByteArray wrap(const QByteArray &content, int views);
 | 
						|
 | 
						|
	[[nodiscard]] QByteArray tag(
 | 
						|
		const QByteArray &name,
 | 
						|
		const QByteArray &body = {});
 | 
						|
	[[nodiscard]] QByteArray tag(
 | 
						|
		const QByteArray &name,
 | 
						|
		const Attributes &attributes,
 | 
						|
		const QByteArray &body = {});
 | 
						|
	[[nodiscard]] QByteArray utf(const MTPstring &text);
 | 
						|
	[[nodiscard]] QByteArray utf(const tl::conditional<MTPstring> &text);
 | 
						|
	[[nodiscard]] QByteArray rich(const MTPRichText &text);
 | 
						|
	[[nodiscard]] QByteArray caption(const MTPPageCaption &caption);
 | 
						|
 | 
						|
	[[nodiscard]] Photo parse(const MTPPhoto &photo);
 | 
						|
	[[nodiscard]] Document parse(const MTPDocument &document);
 | 
						|
	[[nodiscard]] Geo parse(const MTPGeoPoint &geo);
 | 
						|
 | 
						|
	[[nodiscard]] Photo photoById(uint64 id);
 | 
						|
	[[nodiscard]] Document documentById(uint64 id);
 | 
						|
 | 
						|
	[[nodiscard]] QByteArray photoFullUrl(const Photo &photo);
 | 
						|
	[[nodiscard]] QByteArray documentFullUrl(const Document &document);
 | 
						|
	[[nodiscard]] QByteArray embedUrl(const QByteArray &html);
 | 
						|
	[[nodiscard]] QByteArray mapUrl(
 | 
						|
		const Geo &geo,
 | 
						|
		int width,
 | 
						|
		int height,
 | 
						|
		int zoom);
 | 
						|
	[[nodiscard]] QByteArray resource(QByteArray id);
 | 
						|
 | 
						|
	[[nodiscard]] std::vector<QSize> computeCollageDimensions(
 | 
						|
		const QVector<MTPPageBlock> &items);
 | 
						|
	[[nodiscard]] QSize computeSlideshowDimensions(
 | 
						|
		const QVector<MTPPageBlock> &items);
 | 
						|
 | 
						|
	//const Options _options;
 | 
						|
	const QByteArray _fileOriginPostfix;
 | 
						|
 | 
						|
	base::flat_set<QByteArray> _resources;
 | 
						|
 | 
						|
	Prepared _result;
 | 
						|
 | 
						|
	base::flat_map<uint64, Photo> _photosById;
 | 
						|
	base::flat_map<uint64, Document> _documentsById;
 | 
						|
 | 
						|
};
 | 
						|
 | 
						|
[[nodiscard]] bool IsVoidElement(const QByteArray &name) {
 | 
						|
	// Thanks https://developer.mozilla.org/en-US/docs/Glossary/Void_element
 | 
						|
	static const auto voids = base::flat_set<QByteArray>{
 | 
						|
		"area"_q,
 | 
						|
		"base"_q,
 | 
						|
		"br"_q,
 | 
						|
		"col"_q,
 | 
						|
		"embed"_q,
 | 
						|
		"hr"_q,
 | 
						|
		"img"_q,
 | 
						|
		"input"_q,
 | 
						|
		"link"_q,
 | 
						|
		"meta"_q,
 | 
						|
		"source"_q,
 | 
						|
		"track"_q,
 | 
						|
		"wbr"_q,
 | 
						|
	};
 | 
						|
	return voids.contains(name);
 | 
						|
}
 | 
						|
 | 
						|
[[nodiscard]] QByteArray ArrowSvg(bool left) {
 | 
						|
	const auto rotate = QByteArray(left ? "180" : "0");
 | 
						|
	return R"(
 | 
						|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
 | 
						|
	<path
 | 
						|
		d="M14.9972363,18 L9.13865768,12.1414214 C9.06055283,12.0633165 9.06055283,11.9366835 9.13865768,11.8585786 L14.9972363,6 L14.9972363,6"
 | 
						|
		transform="translate(11.997236, 12) scale(-1, -1) rotate()" + rotate + ") translate(-11.997236, -12)" + R"(">
 | 
						|
	</path>
 | 
						|
</svg>)";
 | 
						|
}
 | 
						|
 | 
						|
Parser::Parser(const Source &source, const Options &options)
 | 
						|
: /*_options(options)
 | 
						|
, */_fileOriginPostfix('/' + Number(source.pageId)) {
 | 
						|
	process(source);
 | 
						|
	_result.pageId = source.pageId;
 | 
						|
	_result.name = source.name;
 | 
						|
	_result.rtl = source.page.data().is_rtl();
 | 
						|
 | 
						|
	const auto views = source.page.data().vviews().value_or_empty();
 | 
						|
	const auto content = list(source.page.data().vblocks());
 | 
						|
	_result.content = wrap(content, views);
 | 
						|
}
 | 
						|
 | 
						|
Prepared Parser::result() {
 | 
						|
	return _result;
 | 
						|
}
 | 
						|
 | 
						|
void Parser::process(const Source &source) {
 | 
						|
	const auto &data = source.page.data();
 | 
						|
	for (const auto &photo : data.vphotos().v) {
 | 
						|
		process(photo);
 | 
						|
	}
 | 
						|
	for (const auto &document : data.vdocuments().v) {
 | 
						|
		process(document);
 | 
						|
	}
 | 
						|
	if (source.webpagePhoto) {
 | 
						|
		process(*source.webpagePhoto);
 | 
						|
	}
 | 
						|
	if (source.webpageDocument) {
 | 
						|
		process(*source.webpageDocument);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void Parser::process(const MTPPhoto &photo) {
 | 
						|
	_photosById.emplace(
 | 
						|
		photo.match([](const auto &data) { return data.vid().v; }),
 | 
						|
		parse(photo));
 | 
						|
}
 | 
						|
 | 
						|
void Parser::process(const MTPDocument &document) {
 | 
						|
	_documentsById.emplace(
 | 
						|
		document.match([](const auto &data) { return data.vid().v; }),
 | 
						|
		parse(document));
 | 
						|
}
 | 
						|
 | 
						|
template <typename Inner>
 | 
						|
QByteArray Parser::list(const MTPVector<Inner> &data) {
 | 
						|
	auto result = QByteArrayList();
 | 
						|
	result.reserve(data.v.size());
 | 
						|
	for (const auto &item : data.v) {
 | 
						|
		result.append(item.match([&](const auto &data) {
 | 
						|
			return block(data);
 | 
						|
		}));
 | 
						|
	}
 | 
						|
	return result.join(QByteArray());
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::collage(
 | 
						|
		const QVector<MTPPageBlock> &list,
 | 
						|
		const std::vector<QSize> &dimensions,
 | 
						|
		int offset) {
 | 
						|
	Expects(list.size() == dimensions.size());
 | 
						|
 | 
						|
	constexpr auto kPerCollage = 10;
 | 
						|
	const auto last = (offset + kPerCollage >= int(dimensions.size()));
 | 
						|
 | 
						|
	auto result = QByteArray();
 | 
						|
	auto slice = ((offset > 0) || (dimensions.size() > kPerCollage))
 | 
						|
		? (dimensions
 | 
						|
			| ranges::views::drop(offset)
 | 
						|
			| ranges::views::take(kPerCollage)
 | 
						|
			| ranges::to_vector)
 | 
						|
		: dimensions;
 | 
						|
	const auto layout = Ui::LayoutMediaGroup(
 | 
						|
		slice,
 | 
						|
		st::historyGroupWidthMax,
 | 
						|
		st::historyGroupWidthMin,
 | 
						|
		st::historyGroupSkip);
 | 
						|
	auto size = QSize();
 | 
						|
	for (const auto &part : layout) {
 | 
						|
		const auto &rect = part.geometry;
 | 
						|
		size = QSize(
 | 
						|
			std::max(size.width(), rect.x() + rect.width()),
 | 
						|
			std::max(size.height(), rect.y() + rect.height()));
 | 
						|
	}
 | 
						|
	for (auto i = 0, count = int(layout.size()); i != count; ++i) {
 | 
						|
		const auto &part = layout[i];
 | 
						|
		list[offset + i].match([&](const MTPDpageBlockPhoto &data) {
 | 
						|
			result += block(data, part, size);
 | 
						|
		}, [&](const MTPDpageBlockVideo &data) {
 | 
						|
			result += block(data, part, size);
 | 
						|
		}, [](const auto &) {
 | 
						|
			Unexpected("Block type in collage layout.");
 | 
						|
		});
 | 
						|
	}
 | 
						|
	const auto aspectHeight = size.height() / float64(size.width());
 | 
						|
	const auto aspectSkip = st::historyGroupSkip / float64(size.width());
 | 
						|
	auto wrapped = tag("figure", {
 | 
						|
		{ "class", "collage" },
 | 
						|
		{
 | 
						|
			"style",
 | 
						|
			("padding-top: " + Percent(aspectHeight) + "%; "
 | 
						|
				+ "margin-bottom: " + Percent(last ? 0 : aspectSkip) + "%;")
 | 
						|
		},
 | 
						|
	}, result);
 | 
						|
	if (offset + kPerCollage < int(dimensions.size())) {
 | 
						|
		wrapped += collage(list, dimensions, offset + kPerCollage);
 | 
						|
	}
 | 
						|
	return wrapped;
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::slideshow(
 | 
						|
		const QVector<MTPPageBlock> &list,
 | 
						|
		QSize dimensions) {
 | 
						|
	auto result = QByteArray();
 | 
						|
	for (auto i = 0, count = int(list.size()); i != count; ++i) {
 | 
						|
		list[i].match([&](const MTPDpageBlockPhoto &data) {
 | 
						|
			result += block(data, {}, dimensions);
 | 
						|
		}, [&](const MTPDpageBlockVideo &data) {
 | 
						|
			result += block(data, {}, dimensions);
 | 
						|
		}, [](const auto &) {
 | 
						|
			Unexpected("Block type in collage layout.");
 | 
						|
		});
 | 
						|
	}
 | 
						|
 | 
						|
	auto inputs = QByteArrayList();
 | 
						|
	for (auto i = 0; i != int(list.size()); ++i) {
 | 
						|
		auto attributes = Attributes{
 | 
						|
			{ "type", "radio" },
 | 
						|
			{ "name", "s" },
 | 
						|
			{ "value", Number(i) },
 | 
						|
			{ "onchange", "return IV.slideshowSlide(this);" },
 | 
						|
		};
 | 
						|
		if (!i) {
 | 
						|
			attributes.push_back({ "checked", std::nullopt });
 | 
						|
		}
 | 
						|
		inputs.append(tag("label", tag("input", attributes, tag("i"))));
 | 
						|
	}
 | 
						|
	const auto form = tag(
 | 
						|
		"form",
 | 
						|
		{ { "class", "slideshow-buttons" } },
 | 
						|
		tag("fieldset", inputs.join(QByteArray())));
 | 
						|
	const auto navigation = tag("a", {
 | 
						|
		{ "class", "slideshow-prev" },
 | 
						|
		{ "onclick", "IV.slideshowSlide(this, -1);" },
 | 
						|
	}, ArrowSvg(true)) + tag("a", {
 | 
						|
		{ "class", "slideshow-next" },
 | 
						|
		{ "onclick", "IV.slideshowSlide(this, 1);" },
 | 
						|
	}, ArrowSvg(false));
 | 
						|
	auto wrapStyle = "padding-top: calc(min("
 | 
						|
		+ Percent(dimensions.height() / float64(dimensions.width()))
 | 
						|
		+ "%, 480px));";
 | 
						|
	result = form + tag("figure", {
 | 
						|
		{ "class", "slideshow" },
 | 
						|
	}, result) + navigation;
 | 
						|
	return tag("figure", {
 | 
						|
		{ "class", "slideshow-wrap" },
 | 
						|
		{ "style", wrapStyle },
 | 
						|
	}, result);
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockUnsupported &data) {
 | 
						|
	return QByteArray();
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockTitle &data) {
 | 
						|
	return tag("h1", { { "class", "title" } }, rich(data.vtext()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockSubtitle &data) {
 | 
						|
	return tag("h2", { { "class", "subtitle" } }, rich(data.vtext()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockAuthorDate &data) {
 | 
						|
	auto inner = rich(data.vauthor());
 | 
						|
	if (const auto date = data.vpublished_date().v) {
 | 
						|
		inner += " \xE2\x80\xA2 " + tag("time", Date(date));
 | 
						|
	}
 | 
						|
	return tag("address", inner);
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockHeader &data) {
 | 
						|
	return tag("h3", { { "class", "header" } }, rich(data.vtext()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockSubheader &data) {
 | 
						|
	return tag("h4", { { "class", "subheader" } }, rich(data.vtext()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockParagraph &data) {
 | 
						|
	return tag("p", rich(data.vtext()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockPreformatted &data) {
 | 
						|
	auto list = Attributes();
 | 
						|
	const auto language = utf(data.vlanguage());
 | 
						|
	if (!language.isEmpty()) {
 | 
						|
		list.push_back({ "data-language", language });
 | 
						|
		list.push_back({ "class", "lang-" + language });
 | 
						|
		_result.hasCode = true;
 | 
						|
	}
 | 
						|
	return tag("pre", list, rich(data.vtext()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockFooter &data) {
 | 
						|
	return tag("footer", { { "class", "footer" } }, rich(data.vtext()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockDivider &data) {
 | 
						|
	return tag("hr", { { "class", "divider" } });
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockAnchor &data) {
 | 
						|
	return tag("a", { { "name", utf(data.vname()) } });
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockList &data) {
 | 
						|
	return tag("ul", list(data.vitems()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockBlockquote &data) {
 | 
						|
	const auto caption = rich(data.vcaption());
 | 
						|
	const auto cite = caption.isEmpty()
 | 
						|
		? QByteArray()
 | 
						|
		: tag("cite", caption);
 | 
						|
	return tag("blockquote", rich(data.vtext()) + cite);
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockPullquote &data) {
 | 
						|
	const auto caption = rich(data.vcaption());
 | 
						|
	const auto cite = caption.isEmpty()
 | 
						|
		? QByteArray()
 | 
						|
		: tag("cite", caption);
 | 
						|
	return tag(
 | 
						|
		"div",
 | 
						|
		{ { "class", "pullquote" } },
 | 
						|
		rich(data.vtext()) + cite);
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(
 | 
						|
		const MTPDpageBlockPhoto &data,
 | 
						|
		const Ui::GroupMediaLayout &layout,
 | 
						|
		QSize outer) {
 | 
						|
	const auto collage = !layout.geometry.isEmpty();
 | 
						|
	const auto slideshow = !collage && !outer.isEmpty();
 | 
						|
	const auto photo = photoById(data.vphoto_id().v);
 | 
						|
	if (!photo.id) {
 | 
						|
		return "Photo not found.";
 | 
						|
	}
 | 
						|
	const auto src = photoFullUrl(photo);
 | 
						|
	auto wrapStyle = QByteArray();
 | 
						|
	if (collage) {
 | 
						|
		const auto wcoef = 1. / outer.width();
 | 
						|
		const auto hcoef = 1. / outer.height();
 | 
						|
		wrapStyle += "left: " + Percent(layout.geometry.x() * wcoef) + "%; "
 | 
						|
			+ "top: " + Percent(layout.geometry.y() * hcoef) + "%; "
 | 
						|
			+ "width: " + Percent(layout.geometry.width() * wcoef) + "%; "
 | 
						|
			+ "height: " + Percent(layout.geometry.height() * hcoef) + "%";
 | 
						|
	} else if (!slideshow && photo.width) {
 | 
						|
		wrapStyle += "max-width:" + Number(photo.width) + "px";
 | 
						|
	}
 | 
						|
	const auto dimension = collage
 | 
						|
		? (layout.geometry.height() / float64(layout.geometry.width()))
 | 
						|
		: (photo.width && photo.height)
 | 
						|
		? (photo.height / float64(photo.width))
 | 
						|
		: (3 / 4.);
 | 
						|
	const auto paddingTop = collage
 | 
						|
		? Percent(dimension) + "%"
 | 
						|
		: "calc(min(480px, " + Percent(dimension) + "%))";
 | 
						|
	const auto style = "background-image:url('" + src + "');"
 | 
						|
		"padding-top: " + paddingTop + ";";
 | 
						|
	auto inner = tag("div", {
 | 
						|
		{ "class", "photo" },
 | 
						|
		{ "style", style } });
 | 
						|
	const auto minithumb = Images::ExpandInlineBytes(photo.minithumbnail);
 | 
						|
	if (!minithumb.isEmpty()) {
 | 
						|
		const auto image = Images::Read({ .content = minithumb });
 | 
						|
		inner = tag("div", {
 | 
						|
			{ "class", "photo-bg" },
 | 
						|
			{ "style", "background-image:url('data:image/jpeg;base64,"
 | 
						|
				+ minithumb.toBase64()
 | 
						|
				+ "');" },
 | 
						|
		}) + inner;
 | 
						|
	}
 | 
						|
	auto attributes = Attributes{
 | 
						|
		{ "class", "photo-wrap" },
 | 
						|
		{ "style", wrapStyle }
 | 
						|
	};
 | 
						|
	auto result = tag("div", attributes, inner);
 | 
						|
 | 
						|
	const auto href = data.vurl() ? utf(*data.vurl()) : photoFullUrl(photo);
 | 
						|
	const auto id = Number(photo.id);
 | 
						|
	result = tag("a", {
 | 
						|
		{ "href", href },
 | 
						|
		{ "oncontextmenu", data.vurl() ? QByteArray() : "return false;" },
 | 
						|
		{ "data-context", data.vurl() ? QByteArray() : "viewer-photo" + id },
 | 
						|
	}, result);
 | 
						|
	if (!slideshow) {
 | 
						|
		result += caption(data.vcaption());
 | 
						|
		if (!collage) {
 | 
						|
			result = tag("div", { { "class", "media-outer" } }, result);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(
 | 
						|
		const MTPDpageBlockVideo &data,
 | 
						|
		const Ui::GroupMediaLayout &layout,
 | 
						|
		QSize outer) {
 | 
						|
	const auto collage = !layout.geometry.isEmpty();
 | 
						|
	const auto slideshow = !collage && !outer.isEmpty();
 | 
						|
	const auto collageSmall = collage
 | 
						|
		&& (layout.geometry.width() < outer.width());
 | 
						|
	const auto video = documentById(data.vvideo_id().v);
 | 
						|
	if (!video.id) {
 | 
						|
		return "Video not found.";
 | 
						|
	}
 | 
						|
	auto inner = tag("div", {
 | 
						|
		{ "class", "video" },
 | 
						|
		{ "data-src", documentFullUrl(video) },
 | 
						|
		{ "data-autoplay", data.is_autoplay() ? "1" : "0" },
 | 
						|
		{ "data-loop", data.is_loop() ? "1" : "0" },
 | 
						|
		{ "data-small", collageSmall ? "1" : "0" },
 | 
						|
	});
 | 
						|
	const auto minithumb = Images::ExpandInlineBytes(video.minithumbnail);
 | 
						|
	if (!minithumb.isEmpty()) {
 | 
						|
		const auto image = Images::Read({ .content = minithumb });
 | 
						|
		inner = tag("div", {
 | 
						|
			{ "class", "video-bg" },
 | 
						|
			{ "style", "background-image:url('data:image/jpeg;base64,"
 | 
						|
				+ minithumb.toBase64()
 | 
						|
				+ "');" },
 | 
						|
		}) + inner;
 | 
						|
	}
 | 
						|
	auto wrapStyle = QByteArray();
 | 
						|
	if (collage) {
 | 
						|
		const auto wcoef = 1. / outer.width();
 | 
						|
		const auto hcoef = 1. / outer.height();
 | 
						|
		wrapStyle += "left: " + Percent(layout.geometry.x() * wcoef) + "%; "
 | 
						|
			+ "top: " + Percent(layout.geometry.y() * hcoef) + "%; "
 | 
						|
			+ "width: " + Percent(layout.geometry.width() * wcoef) + "%; "
 | 
						|
			+ "height: " + Percent(layout.geometry.height() * hcoef) + "%; ";
 | 
						|
	} else {
 | 
						|
		const auto dimension = (video.width && video.height)
 | 
						|
			? (video.height / float64(video.width))
 | 
						|
			: (3 / 4.);
 | 
						|
		wrapStyle += "padding-top: calc(min(480px, "
 | 
						|
			+ Percent(dimension)
 | 
						|
			+ "%));";
 | 
						|
	}
 | 
						|
	auto attributes = Attributes{
 | 
						|
		{ "class", "video-wrap" },
 | 
						|
		{ "style", wrapStyle },
 | 
						|
	};
 | 
						|
	auto result = tag("div", attributes, inner);
 | 
						|
	if (data.is_autoplay() || collageSmall) {
 | 
						|
		const auto id = Number(video.id);
 | 
						|
		const auto href = resource("video" + id);
 | 
						|
		result = tag("a", {
 | 
						|
			{ "href", href },
 | 
						|
			{ "oncontextmenu", "return false;" },
 | 
						|
			{ "data-context", "viewer-video" + id },
 | 
						|
		}, result);
 | 
						|
	}
 | 
						|
	if (!slideshow) {
 | 
						|
		result += caption(data.vcaption());
 | 
						|
		if (!collage) {
 | 
						|
			result = tag("div", { { "class", "media-outer" } }, result);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockCover &data) {
 | 
						|
	return tag("figure", data.vcover().match([&](const auto &data) {
 | 
						|
		return block(data);
 | 
						|
	}));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockEmbed &data) {
 | 
						|
	_result.hasEmbeds = true;
 | 
						|
	auto eclass = data.is_full_width() ? QByteArray() : "nowide";
 | 
						|
	auto width = QByteArray();
 | 
						|
	auto height = QByteArray();
 | 
						|
	auto iframeWidth = QByteArray();
 | 
						|
	auto iframeHeight = QByteArray();
 | 
						|
	const auto autosize = !data.vw();
 | 
						|
	if (autosize) {
 | 
						|
		iframeWidth = "100%";
 | 
						|
		eclass = "nowide";
 | 
						|
	} else if (data.is_full_width() || !data.vw()->v) {
 | 
						|
		width = "100%";
 | 
						|
		height = Number(data.vh()->v) + "px";
 | 
						|
		iframeWidth = "100%";
 | 
						|
		iframeHeight = height;
 | 
						|
	} else {
 | 
						|
		width = Number(data.vw()->v) + "px";
 | 
						|
		height = Percent(data.vh()->v / float64(data.vw()->v)) + "%";
 | 
						|
	}
 | 
						|
	auto attributes = Attributes();
 | 
						|
	if (autosize) {
 | 
						|
		attributes.push_back({ "class", "autosize" });
 | 
						|
	}
 | 
						|
	attributes.push_back({ "width", iframeWidth });
 | 
						|
	attributes.push_back({ "height", iframeHeight });
 | 
						|
	if (const auto url = data.vurl()) {
 | 
						|
		if (!autosize) {
 | 
						|
			attributes.push_back({ "src", utf(url) });
 | 
						|
		} else {
 | 
						|
			attributes.push_back({ "srcdoc", utf(url) });
 | 
						|
		}
 | 
						|
	} else if (const auto html = data.vhtml()) {
 | 
						|
		attributes.push_back({ "src", embedUrl(html->v) });
 | 
						|
	}
 | 
						|
	if (!data.is_allow_scrolling()) {
 | 
						|
		attributes.push_back({ "scrolling", "no" });
 | 
						|
	}
 | 
						|
	attributes.push_back({ "frameborder", "0" });
 | 
						|
	attributes.push_back({ "allowtransparency", "true" });
 | 
						|
	attributes.push_back({ "allowfullscreen", "true" });
 | 
						|
	auto result = tag("iframe", attributes);
 | 
						|
	if (!autosize) {
 | 
						|
		result = tag("div", {
 | 
						|
			{ "class", "iframe-wrap" },
 | 
						|
			{ "style", "width:" + width },
 | 
						|
		}, tag("div", {
 | 
						|
			{ "style", "padding-bottom: " + height },
 | 
						|
		}, result));
 | 
						|
	}
 | 
						|
	result += caption(data.vcaption());
 | 
						|
	return tag("figure", { { "class", eclass } }, result);
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockEmbedPost &data) {
 | 
						|
	auto result = QByteArray();
 | 
						|
	if (!data.vblocks().v.isEmpty()) {
 | 
						|
		auto address = QByteArray();
 | 
						|
		const auto photo = photoById(data.vauthor_photo_id().v);
 | 
						|
		if (photo.id) {
 | 
						|
			const auto src = photoFullUrl(photo);
 | 
						|
			address += tag(
 | 
						|
				"figure",
 | 
						|
				{ { "style", "background-image:url('" + src + "')" } });
 | 
						|
		}
 | 
						|
		address += tag(
 | 
						|
			"a",
 | 
						|
			{ { "rel", "author" }, { "onclick", "return false;" } },
 | 
						|
			utf(data.vauthor()));
 | 
						|
		if (const auto date = data.vdate().v) {
 | 
						|
			const auto parsed = base::unixtime::parse(date);
 | 
						|
			address += tag("time", Date(date));
 | 
						|
		}
 | 
						|
		const auto inner = tag("address", address) + list(data.vblocks());
 | 
						|
		result = tag("blockquote", { { "class", "embed-post" } }, inner);
 | 
						|
	} else {
 | 
						|
		const auto url = utf(data.vurl());
 | 
						|
		const auto inner = tag("strong", utf(data.vauthor()))
 | 
						|
			+ tag(
 | 
						|
				"small",
 | 
						|
				tag("a", { { "href", url } }, url));
 | 
						|
		result = tag("section", { { "class", "embed-post" } }, inner);
 | 
						|
	}
 | 
						|
	result += caption(data.vcaption());
 | 
						|
	return tag("figure", result);
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockCollage &data) {
 | 
						|
	const auto &items = data.vitems().v;
 | 
						|
	const auto dimensions = computeCollageDimensions(items);
 | 
						|
	if (dimensions.empty()) {
 | 
						|
		return tag(
 | 
						|
			"figure",
 | 
						|
			tag("figure", list(data.vitems())) + caption(data.vcaption()));
 | 
						|
	}
 | 
						|
 | 
						|
	return tag(
 | 
						|
		"figure",
 | 
						|
		{ { "class", "collage-wrap" } },
 | 
						|
		collage(items, dimensions) + caption(data.vcaption()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockSlideshow &data) {
 | 
						|
	const auto &items = data.vitems().v;
 | 
						|
	const auto dimensions = computeSlideshowDimensions(items);
 | 
						|
	if (dimensions.isEmpty()) {
 | 
						|
		return list(data.vitems());
 | 
						|
	}
 | 
						|
	const auto result = slideshow(items, dimensions);
 | 
						|
	return tag("figure", result + caption(data.vcaption()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockChannel &data) {
 | 
						|
	auto name = QByteArray();
 | 
						|
	auto username = QByteArray();
 | 
						|
	auto id = data.vchannel().match([](const auto &data) {
 | 
						|
		return Number(data.vid().v);
 | 
						|
	});
 | 
						|
	data.vchannel().match([&](const MTPDchannel &data) {
 | 
						|
		if (const auto has = data.vusername()) {
 | 
						|
			username = utf(*has);
 | 
						|
		}
 | 
						|
		name = utf(data.vtitle());
 | 
						|
	}, [&](const MTPDchat &data) {
 | 
						|
		name = utf(data.vtitle());
 | 
						|
	}, [](const auto &) {
 | 
						|
	});
 | 
						|
	auto result = tag(
 | 
						|
		"div",
 | 
						|
		{ { "class", "join" }, { "data-context", "join_link" + id } },
 | 
						|
		tag("span")
 | 
						|
	) + tag("h4", name);
 | 
						|
	const auto link = username.isEmpty()
 | 
						|
		? "javascript:alert('Channel Link');"
 | 
						|
		: "https://t.me/" + username;
 | 
						|
	result = tag(
 | 
						|
		"a",
 | 
						|
		{ { "href", link }, { "data-context", "channel" + id } },
 | 
						|
		result);
 | 
						|
	_result.channelIds.emplace(id);
 | 
						|
	return tag("section", {
 | 
						|
		{ "class", "channel joined" },
 | 
						|
		{ "data-context", "channel" + id },
 | 
						|
	}, result);
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockAudio &data) {
 | 
						|
	const auto audio = documentById(data.vaudio_id().v);
 | 
						|
	if (!audio.id) {
 | 
						|
		return "Audio not found.";
 | 
						|
	}
 | 
						|
	const auto src = documentFullUrl(audio);
 | 
						|
	return tag("figure", tag("audio", {
 | 
						|
		{ "src", src },
 | 
						|
		{ "oncontextmenu", "return false;" },
 | 
						|
		{ "controls", std::nullopt },
 | 
						|
	}) + caption(data.vcaption()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockKicker &data) {
 | 
						|
	return tag("h5", { { "class", "kicker" } }, rich(data.vtext()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockTable &data) {
 | 
						|
	auto classes = QByteArrayList();
 | 
						|
	if (data.is_bordered()) {
 | 
						|
		classes.push_back("bordered");
 | 
						|
	}
 | 
						|
	if (data.is_striped()) {
 | 
						|
		classes.push_back("striped");
 | 
						|
	}
 | 
						|
	auto attibutes = Attributes();
 | 
						|
	if (!classes.isEmpty()) {
 | 
						|
		attibutes.push_back({ "class", classes.join(" ") });
 | 
						|
	}
 | 
						|
	auto title = rich(data.vtitle());
 | 
						|
	if (!title.isEmpty()) {
 | 
						|
		title = tag("caption", title);
 | 
						|
	}
 | 
						|
	auto result = tag("table", attibutes, title + list(data.vrows()));
 | 
						|
	result = tag("figure", { { "class", "table" } }, result);
 | 
						|
	result = tag("figure", { { "class", "table-wrap" } }, result);
 | 
						|
	return tag("figure", result);
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockOrderedList &data) {
 | 
						|
	return tag("ol", list(data.vitems()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockDetails &data) {
 | 
						|
	auto attributes = Attributes();
 | 
						|
	if (data.is_open()) {
 | 
						|
		attributes.push_back({ "open", std::nullopt });
 | 
						|
	}
 | 
						|
	return tag(
 | 
						|
		"details",
 | 
						|
		attributes,
 | 
						|
		tag("summary", rich(data.vtitle())) + list(data.vblocks()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockRelatedArticles &data) {
 | 
						|
	const auto result = list(data.varticles());
 | 
						|
	if (result.isEmpty()) {
 | 
						|
		return QByteArray();
 | 
						|
	}
 | 
						|
	auto title = rich(data.vtitle());
 | 
						|
	if (!title.isEmpty()) {
 | 
						|
		title = tag("h4", { { "class", "related-title" } }, title);
 | 
						|
	}
 | 
						|
	return tag("section", { { "class", "related" } }, title + result);
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageBlockMap &data) {
 | 
						|
	const auto geo = parse(data.vgeo());
 | 
						|
	if (!geo.access) {
 | 
						|
		return "Map not found.";
 | 
						|
	}
 | 
						|
	const auto width = 650;
 | 
						|
	const auto height = std::min(450, (data.vh().v * width / data.vw().v));
 | 
						|
	return tag("figure", tag("img", {
 | 
						|
		{ "src", mapUrl(geo, width, height, data.vzoom().v) },
 | 
						|
	}) + caption(data.vcaption()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageRelatedArticle &data) {
 | 
						|
	auto result = QByteArray();
 | 
						|
	const auto photo = photoById(data.vphoto_id().value_or_empty());
 | 
						|
	if (photo.id) {
 | 
						|
		const auto src = photoFullUrl(photo);
 | 
						|
		result += tag("i", {
 | 
						|
			{ "class", "related-link-thumb" },
 | 
						|
			{ "style", "background-image:url('" + src + "')" },
 | 
						|
		});
 | 
						|
	}
 | 
						|
	const auto title = data.vtitle();
 | 
						|
	const auto description = data.vdescription();
 | 
						|
	const auto author = data.vauthor();
 | 
						|
	const auto published = data.vpublished_date();
 | 
						|
	if (title || description || author || published) {
 | 
						|
		auto inner = QByteArray();
 | 
						|
		if (title) {
 | 
						|
			inner += tag(
 | 
						|
				"span",
 | 
						|
				{ { "class", "related-link-title" } },
 | 
						|
				utf(*title));
 | 
						|
		}
 | 
						|
		if (description) {
 | 
						|
			inner += tag(
 | 
						|
				"span",
 | 
						|
				{ { "class", "related-link-desc" } },
 | 
						|
				utf(*description));
 | 
						|
		}
 | 
						|
		if (author || published) {
 | 
						|
			inner += tag(
 | 
						|
				"span",
 | 
						|
				{ { "class", "related-link-source" } },
 | 
						|
				((author ? utf(*author) : QByteArray())
 | 
						|
					+ ((author && published) ? ", " : QByteArray())
 | 
						|
					+ (published ? Date(published->v) : QByteArray())));
 | 
						|
		}
 | 
						|
		result += tag("span", {
 | 
						|
			{ "class", "related-link-content" },
 | 
						|
		}, inner);
 | 
						|
	}
 | 
						|
	const auto webpageId = data.vwebpage_id().v;
 | 
						|
	const auto context = webpageId
 | 
						|
		? ("webpage" + Number(webpageId))
 | 
						|
		: QByteArray();
 | 
						|
	return tag("a", {
 | 
						|
		{ "class", "related-link" },
 | 
						|
		{ "href", utf(data.vurl()) },
 | 
						|
		{ "data-context", context },
 | 
						|
	}, result);
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageTableRow &data) {
 | 
						|
	return tag("tr", list(data.vcells()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageTableCell &data) {
 | 
						|
	const auto text = data.vtext() ? rich(*data.vtext()) : QByteArray();
 | 
						|
	auto style = QByteArray();
 | 
						|
	if (data.is_align_right()) {
 | 
						|
		style += "text-align:right;";
 | 
						|
	} else if (data.is_align_center()) {
 | 
						|
		style += "text-align:center;";
 | 
						|
	} else {
 | 
						|
		style += "text-align:left;";
 | 
						|
	}
 | 
						|
	if (data.is_valign_bottom()) {
 | 
						|
		style += "vertical-align:bottom;";
 | 
						|
	} else if (data.is_valign_middle()) {
 | 
						|
		style += "vertical-align:middle;";
 | 
						|
	} else {
 | 
						|
		style += "vertical-align:top;";
 | 
						|
	}
 | 
						|
	auto attributes = Attributes{ { "style", style } };
 | 
						|
	if (const auto cs = data.vcolspan()) {
 | 
						|
		attributes.push_back({ "colspan", Number(cs->v) });
 | 
						|
	}
 | 
						|
	if (const auto rs = data.vrowspan()) {
 | 
						|
		attributes.push_back({ "rowspan", Number(rs->v) });
 | 
						|
	}
 | 
						|
	return tag(data.is_header() ? "th" : "td", attributes, text);
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageListItemText &data) {
 | 
						|
	return tag("li", rich(data.vtext()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageListItemBlocks &data) {
 | 
						|
	return tag("li", list(data.vblocks()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageListOrderedItemText &data) {
 | 
						|
	return tag(
 | 
						|
		"li",
 | 
						|
		{ { "value", utf(data.vnum()) } },
 | 
						|
		rich(data.vtext()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::block(const MTPDpageListOrderedItemBlocks &data) {
 | 
						|
	return tag(
 | 
						|
		"li",
 | 
						|
		{ { "value", utf(data.vnum()) } },
 | 
						|
		list(data.vblocks()));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::utf(const MTPstring &text) {
 | 
						|
	return Escape(text.v);
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::utf(const tl::conditional<MTPstring> &text) {
 | 
						|
	return text ? utf(*text) : QByteArray();
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::wrap(const QByteArray &content, int views) {
 | 
						|
	const auto sep = " \xE2\x80\xA2 ";
 | 
						|
	const auto viewsText = views
 | 
						|
		? (tr::lng_stories_views(tr::now, lt_count_decimal, views) + sep)
 | 
						|
		: QString();
 | 
						|
	return R"(
 | 
						|
<div class="page-slide">
 | 
						|
	<article>)"_q + content + R"(</article>
 | 
						|
</div>
 | 
						|
<div class="page-footer">
 | 
						|
	<div class="content">
 | 
						|
		)"_q
 | 
						|
		+ viewsText.toUtf8()
 | 
						|
		+ R"(<a class="wrong" data-context="report-iv">)"_q
 | 
						|
		+ tr::lng_iv_wrong_layout(tr::now).toUtf8()
 | 
						|
		+ R"(</a>
 | 
						|
	</div>
 | 
						|
</div>)"_q;
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::tag(
 | 
						|
		const QByteArray &name,
 | 
						|
		const QByteArray &body) {
 | 
						|
	return tag(name, {}, body);
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::tag(
 | 
						|
		const QByteArray &name,
 | 
						|
		const Attributes &attributes,
 | 
						|
		const QByteArray &body) {
 | 
						|
	auto list = QByteArrayList();
 | 
						|
	list.reserve(attributes.size());
 | 
						|
	for (auto &[name, value] : attributes) {
 | 
						|
		list.push_back(' ' + name + (value ? "=\"" + *value + "\"" : ""));
 | 
						|
	}
 | 
						|
	const auto serialized = list.join(QByteArray());
 | 
						|
	return (IsVoidElement(name) && body.isEmpty())
 | 
						|
		? ('<' + name + serialized + " />")
 | 
						|
		: ('<' + name + serialized + '>' + body + "</" + name + '>');
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::rich(const MTPRichText &text) {
 | 
						|
	return text.match([&](const MTPDtextEmpty &data) {
 | 
						|
		return QByteArray();
 | 
						|
	}, [&](const MTPDtextPlain &data) {
 | 
						|
		struct Replacement {
 | 
						|
			QByteArray from;
 | 
						|
			QByteArray to;
 | 
						|
		};
 | 
						|
		const auto replacements = std::vector<Replacement>{
 | 
						|
			{ "\xE2\x81\xA6", "<span dir=\"ltr\">" },
 | 
						|
			{ "\xE2\x81\xA7", "<span dir=\"rtl\">" },
 | 
						|
			{ "\xE2\x81\xA8", "<span dir=\"auto\">" },
 | 
						|
			{ "\xE2\x81\xA9", "</span>" },
 | 
						|
		};
 | 
						|
		auto text = utf(data.vtext());
 | 
						|
		for (const auto &[from, to] : replacements) {
 | 
						|
			text.replace(from, to);
 | 
						|
		}
 | 
						|
		return text;
 | 
						|
	}, [&](const MTPDtextConcat &data) {
 | 
						|
		const auto &list = data.vtexts().v;
 | 
						|
		auto result = QByteArrayList();
 | 
						|
		result.reserve(list.size());
 | 
						|
		for (const auto &item : list) {
 | 
						|
			result.append(rich(item));
 | 
						|
		}
 | 
						|
		return result.join(QByteArray());
 | 
						|
	}, [&](const MTPDtextImage &data) {
 | 
						|
		const auto image = documentById(data.vdocument_id().v);
 | 
						|
		if (!image.id) {
 | 
						|
			return "Image not found."_q;
 | 
						|
		}
 | 
						|
		auto attributes = Attributes{
 | 
						|
			{ "class", "pic" },
 | 
						|
			{ "src", documentFullUrl(image) },
 | 
						|
		};
 | 
						|
		if (const auto width = data.vw().v) {
 | 
						|
			attributes.push_back({ "width", Number(width) });
 | 
						|
		}
 | 
						|
		if (const auto height = data.vh().v) {
 | 
						|
			attributes.push_back({ "height", Number(height) });
 | 
						|
		}
 | 
						|
		return tag("img", attributes);
 | 
						|
	}, [&](const MTPDtextBold &data) {
 | 
						|
		return tag("b", rich(data.vtext()));
 | 
						|
	}, [&](const MTPDtextItalic &data) {
 | 
						|
		return tag("i", rich(data.vtext()));
 | 
						|
	}, [&](const MTPDtextUnderline &data) {
 | 
						|
		return tag("u", rich(data.vtext()));
 | 
						|
	}, [&](const MTPDtextStrike &data) {
 | 
						|
		return tag("s", rich(data.vtext()));
 | 
						|
	}, [&](const MTPDtextFixed &data) {
 | 
						|
		return tag("code", rich(data.vtext()));
 | 
						|
	}, [&](const MTPDtextUrl &data) {
 | 
						|
		const auto webpageId = data.vwebpage_id().v;
 | 
						|
		const auto context = webpageId
 | 
						|
			? ("webpage" + Number(webpageId))
 | 
						|
			: QByteArray();
 | 
						|
		return tag("a", {
 | 
						|
			{ "href", utf(data.vurl()) },
 | 
						|
			{ "class", webpageId ? "internal-iv-link" : "" },
 | 
						|
			{ "data-context", context },
 | 
						|
		}, rich(data.vtext()));
 | 
						|
	}, [&](const MTPDtextEmail &data) {
 | 
						|
		return tag("a", {
 | 
						|
			{ "href", "mailto:" + utf(data.vemail()) },
 | 
						|
		}, rich(data.vtext()));
 | 
						|
	}, [&](const MTPDtextSubscript &data) {
 | 
						|
		return tag("sub", rich(data.vtext()));
 | 
						|
	}, [&](const MTPDtextSuperscript &data) {
 | 
						|
		return tag("sup", rich(data.vtext()));
 | 
						|
	}, [&](const MTPDtextMarked &data) {
 | 
						|
		return tag("mark", rich(data.vtext()));
 | 
						|
	}, [&](const MTPDtextPhone &data) {
 | 
						|
		return tag("a", {
 | 
						|
			{ "href", "tel:" + utf(data.vphone()) },
 | 
						|
		}, rich(data.vtext()));
 | 
						|
	}, [&](const MTPDtextAnchor &data) {
 | 
						|
		const auto inner = rich(data.vtext());
 | 
						|
		const auto name = utf(data.vname());
 | 
						|
		return inner.isEmpty()
 | 
						|
			? tag("a", { { "name", name } })
 | 
						|
			: tag(
 | 
						|
				"span",
 | 
						|
				{ { "class", "reference" } },
 | 
						|
				tag("a", { { "name", name } }) + inner);
 | 
						|
	});
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::caption(const MTPPageCaption &caption) {
 | 
						|
	auto text = rich(caption.data().vtext());
 | 
						|
	const auto credit = rich(caption.data().vcredit());
 | 
						|
	if (!credit.isEmpty()) {
 | 
						|
		text += tag("cite", credit);
 | 
						|
	} else if (text.isEmpty()) {
 | 
						|
		return QByteArray();
 | 
						|
	}
 | 
						|
	return tag("figcaption", text);
 | 
						|
}
 | 
						|
 | 
						|
Photo Parser::parse(const MTPPhoto &photo) {
 | 
						|
	auto result = Photo{
 | 
						|
		.id = photo.match([&](const auto &d) { return d.vid().v; }),
 | 
						|
	};
 | 
						|
	auto sizes = base::flat_map<QByteArray, QSize>();
 | 
						|
	photo.match([](const MTPDphotoEmpty &) {
 | 
						|
	}, [&](const MTPDphoto &data) {
 | 
						|
		for (const auto &size : data.vsizes().v) {
 | 
						|
			size.match([&](const MTPDphotoSizeEmpty &data) {
 | 
						|
			}, [&](const MTPDphotoSize &data) {
 | 
						|
				sizes.emplace(
 | 
						|
					data.vtype().v,
 | 
						|
					QSize(data.vw().v, data.vh().v));
 | 
						|
			}, [&](const MTPDphotoCachedSize &data) {
 | 
						|
				sizes.emplace(
 | 
						|
					data.vtype().v,
 | 
						|
					QSize(data.vw().v, data.vh().v));
 | 
						|
			}, [&](const MTPDphotoStrippedSize &data) {
 | 
						|
				result.minithumbnail = data.vbytes().v;
 | 
						|
			}, [&](const MTPDphotoSizeProgressive &data) {
 | 
						|
				sizes.emplace(
 | 
						|
					data.vtype().v,
 | 
						|
					QSize(data.vw().v, data.vh().v));
 | 
						|
			}, [&](const MTPDphotoPathSize &data) {
 | 
						|
			});
 | 
						|
		}
 | 
						|
	});
 | 
						|
	for (const auto attempt : { "y", "x", "w" }) {
 | 
						|
		const auto i = sizes.find(QByteArray(attempt));
 | 
						|
		if (i != end(sizes)) {
 | 
						|
			result.width = i->second.width();
 | 
						|
			result.height = i->second.height();
 | 
						|
			break;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
Document Parser::parse(const MTPDocument &document) {
 | 
						|
	auto result = Document{
 | 
						|
		.id = document.match([&](const auto &d) { return d.vid().v; }),
 | 
						|
	};
 | 
						|
	document.match([](const MTPDdocumentEmpty &) {
 | 
						|
	}, [&](const MTPDdocument &data) {
 | 
						|
		for (const auto &attribute : data.vattributes().v) {
 | 
						|
			attribute.match([&](const MTPDdocumentAttributeImageSize &data) {
 | 
						|
				result.width = data.vw().v;
 | 
						|
				result.height = data.vh().v;
 | 
						|
			}, [&](const MTPDdocumentAttributeVideo &data) {
 | 
						|
				result.width = data.vw().v;
 | 
						|
				result.height = data.vh().v;
 | 
						|
			}, [](const auto &) {});
 | 
						|
		}
 | 
						|
		if (const auto sizes = data.vthumbs()) {
 | 
						|
			for (const auto &size : sizes->v) {
 | 
						|
				size.match([&](const MTPDphotoStrippedSize &data) {
 | 
						|
					result.minithumbnail = data.vbytes().v;
 | 
						|
				}, [&](const auto &data) {
 | 
						|
				});
 | 
						|
			}
 | 
						|
 | 
						|
		}
 | 
						|
	});
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
Geo Parser::parse(const MTPGeoPoint &geo) {
 | 
						|
	return geo.match([](const MTPDgeoPointEmpty &) {
 | 
						|
		return Geo();
 | 
						|
	}, [&](const MTPDgeoPoint &data) {
 | 
						|
		return Geo{
 | 
						|
			.lat = data.vlat().v,
 | 
						|
			.lon = data.vlong().v,
 | 
						|
			.access = data.vaccess_hash().v,
 | 
						|
		};
 | 
						|
	});
 | 
						|
}
 | 
						|
 | 
						|
Photo Parser::photoById(uint64 id) {
 | 
						|
	const auto i = _photosById.find(id);
 | 
						|
	return (i != end(_photosById)) ? i->second : Photo();
 | 
						|
}
 | 
						|
 | 
						|
Document Parser::documentById(uint64 id) {
 | 
						|
	const auto i = _documentsById.find(id);
 | 
						|
	return (i != end(_documentsById)) ? i->second : Document();
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::photoFullUrl(const Photo &photo) {
 | 
						|
	return resource("photo/" + Number(photo.id) + _fileOriginPostfix);
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::documentFullUrl(const Document &document) {
 | 
						|
	return resource("document/" + Number(document.id) + _fileOriginPostfix);
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::embedUrl(const QByteArray &html) {
 | 
						|
	auto binary = std::array<uchar, SHA256_DIGEST_LENGTH>{};
 | 
						|
	SHA256(
 | 
						|
		reinterpret_cast<const unsigned char*>(html.data()),
 | 
						|
		html.size(),
 | 
						|
		binary.data());
 | 
						|
	const auto hex = [](uchar value) -> char {
 | 
						|
		return (value >= 10) ? ('a' + (value - 10)) : ('0' + value);
 | 
						|
	};
 | 
						|
	auto result = QByteArray();
 | 
						|
	result.reserve(binary.size() * 2);
 | 
						|
	for (const auto byte : binary) {
 | 
						|
		result.push_back(hex(byte / 16));
 | 
						|
		result.push_back(hex(byte % 16));
 | 
						|
	}
 | 
						|
	result += ".html";
 | 
						|
	_result.embeds.emplace(result, html);
 | 
						|
	return resource("html/" + result);
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::mapUrl(const Geo &geo, int width, int height, int zoom) {
 | 
						|
	return resource("map/"
 | 
						|
		+ GeoPointId(geo) + "&"
 | 
						|
		+ Number(width) + ","
 | 
						|
		+ Number(height) + "&"
 | 
						|
		+ Number(zoom));
 | 
						|
}
 | 
						|
 | 
						|
QByteArray Parser::resource(QByteArray id) {
 | 
						|
	return '/' + id;
 | 
						|
}
 | 
						|
 | 
						|
std::vector<QSize> Parser::computeCollageDimensions(
 | 
						|
		const QVector<MTPPageBlock> &items) {
 | 
						|
	if (items.size() < 2) {
 | 
						|
		return {};
 | 
						|
	}
 | 
						|
	auto result = std::vector<QSize>(items.size());
 | 
						|
	for (auto i = 0, count = int(items.size()); i != count; ++i) {
 | 
						|
		items[i].match([&](const MTPDpageBlockPhoto &data) {
 | 
						|
			const auto photo = photoById(data.vphoto_id().v);
 | 
						|
			if (photo.id && photo.width > 0 && photo.height > 0) {
 | 
						|
				result[i] = QSize(photo.width, photo.height);
 | 
						|
			}
 | 
						|
		}, [&](const MTPDpageBlockVideo &data) {
 | 
						|
			const auto document = documentById(data.vvideo_id().v);
 | 
						|
			if (document.id && document.width > 0 && document.height > 0) {
 | 
						|
				result[i] = QSize(document.width, document.height);
 | 
						|
			}
 | 
						|
		}, [](const auto &) {});
 | 
						|
		if (result[i].isEmpty()) {
 | 
						|
			return {};
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
QSize Parser::computeSlideshowDimensions(
 | 
						|
		const QVector<MTPPageBlock> &items) {
 | 
						|
	if (items.size() < 2) {
 | 
						|
		return {};
 | 
						|
	}
 | 
						|
	auto result = QSize();
 | 
						|
	for (const auto &item : items) {
 | 
						|
		auto size = QSize();
 | 
						|
		item.match([&](const MTPDpageBlockPhoto &data) {
 | 
						|
			const auto photo = photoById(data.vphoto_id().v);
 | 
						|
			if (photo.id && photo.width > 0 && photo.height > 0) {
 | 
						|
				size = QSize(photo.width, photo.height);
 | 
						|
			}
 | 
						|
		}, [&](const MTPDpageBlockVideo &data) {
 | 
						|
			const auto document = documentById(data.vvideo_id().v);
 | 
						|
			if (document.id && document.width > 0 && document.height > 0) {
 | 
						|
				size = QSize(document.width, document.height);
 | 
						|
			}
 | 
						|
		}, [](const auto &) {});
 | 
						|
		if (size.isEmpty()) {
 | 
						|
			return {};
 | 
						|
		} else if (result.height() * size.width()
 | 
						|
			< result.width() * size.height()) {
 | 
						|
			result = size;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
} // namespace
 | 
						|
 | 
						|
Prepared Prepare(const Source &source, const Options &options) {
 | 
						|
	auto parser = Parser(source, options);
 | 
						|
	return parser.result();
 | 
						|
}
 | 
						|
 | 
						|
} // namespace Iv
 |