#include "iv/markdown/iv_markdown_article.h" #include "iv/markdown/iv_markdown_article_text.h" #include "iv/markdown/iv_markdown_document.h" #include "iv/markdown/iv_markdown_math_renderer.h" #include "iv/markdown/iv_markdown_microtex.h" #include "iv/markdown/iv_markdown_parse.h" #include "iv/markdown/iv_markdown_prepare.h" #include "iv/markdown/iv_markdown_prepare_serialize.h" #include "iv/markdown/iv_markdown_view.h" #include "iv/markdown/iv_markdown_view_widget.h" #include "iv/iv_prepare.h" #include "scheme.h" #include "lang/lang_keys.h" #include "spellcheck/spellcheck_highlight_syntax.h" #include "ui/basic_click_handlers.h" #include "ui/chat/chat_style.h" #include "ui/dynamic_image.h" #include "ui/style/style_core.h" #include "ui/style/style_core_scale.h" #include "ui/widgets/rp_window.h" #include "ui/widgets/scroll_area.h" #include "styles/style_iv.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { using namespace Iv::Markdown; struct TestNativeIvChannelContext { uint64 channelId = 0; QString username; }; [[nodiscard]] TestNativeIvChannelContext ParseTestNativeIvChannelContext( const QString &context) { const auto separator = context.indexOf(u'\n'); return { .channelId = (separator >= 0) ? context.mid(0, separator).toULongLong() : context.toULongLong(), .username = (separator >= 0) ? context.mid(separator + 1) : QString(), }; } [[nodiscard]] QString SerializeTestNativeIvChannelContext( uint64 channelId, QString username) { auto result = QString::number(channelId); if (!username.isEmpty()) { result += u"\n"_q + username; } return result; } [[nodiscard]] QString ResolveTestNativeIvChannelUsername( const QString &channelUsername, const QString &contextUsername) { return !channelUsername.isEmpty() ? channelUsername : contextUsername; } struct Args { QString markdownPath; QString latexMarkdownPath; bool dump = false; bool inlineHtml = false; bool ok = true; QString error; }; struct PreparedFixture { QString label; QString path; PreparedDocument parsed; MarkdownArticleContent prepared; }; class TestDynamicImage final : public Ui::DynamicImage { public: explicit TestDynamicImage(QImage frame = QImage()) : _frame(std::move(frame)) { } [[nodiscard]] std::shared_ptr clone() override { return std::make_shared(_frame); } [[nodiscard]] QImage image(int size) override { requestedSizes.push_back(size); return _frame; } void subscribeToUpdates(Fn callback) override { ++subscriptionCount; _callback = std::move(callback); } void setFrame(QImage frame) { _frame = std::move(frame); } void notify() const { if (_callback) { _callback(); } } mutable std::vector requestedSizes; mutable int subscriptionCount = 0; private: QImage _frame; mutable Fn _callback; }; class TestPhotoRuntime final : public PhotoRuntime { public: [[nodiscard]] std::shared_ptr thumbnail( QSize size) const override { thumbnailSizes.push_back(size); return thumbnailImage; } [[nodiscard]] std::shared_ptr full( QSize size) const override { fullSizes.push_back(size); return fullImage; } [[nodiscard]] bool loaded() const override { return loadedValue; } [[nodiscard]] bool loading() const override { return loadingValue; } [[nodiscard]] double progress() const override { return progressValue; } void open(Qt::MouseButton button) const override { openedButtons.push_back(button); } std::shared_ptr thumbnailImage; std::shared_ptr fullImage; bool loadedValue = false; bool loadingValue = false; double progressValue = 0.; mutable std::vector thumbnailSizes; mutable std::vector fullSizes; mutable std::vector openedButtons; }; class TestDocumentRuntime final : public DocumentRuntime { public: [[nodiscard]] std::shared_ptr thumbnail( QSize size) const override { thumbnailSizes.push_back(size); return thumbnailImage; } [[nodiscard]] std::shared_ptr full( QSize size) const override { fullSizes.push_back(size); return fullImage; } [[nodiscard]] bool loaded() const override { return loadedValue; } [[nodiscard]] bool loading() const override { return loadingValue; } [[nodiscard]] double progress() const override { return progressValue; } void open(Qt::MouseButton button) const override { openedButtons.push_back(button); } std::shared_ptr thumbnailImage; std::shared_ptr fullImage; bool loadedValue = false; bool loadingValue = false; double progressValue = 0.; mutable std::vector thumbnailSizes; mutable std::vector fullSizes; mutable std::vector openedButtons; }; class TestMapRuntime final : public MapRuntime { public: [[nodiscard]] std::shared_ptr thumbnail( QSize size) const override { thumbnailSizes.push_back(size); return thumbnailImage; } [[nodiscard]] std::shared_ptr full( QSize size) const override { fullSizes.push_back(size); return fullImage; } [[nodiscard]] bool loaded() const override { return loadedValue; } [[nodiscard]] bool loading() const override { return loadingValue; } [[nodiscard]] double progress() const override { return progressValue; } std::shared_ptr thumbnailImage; std::shared_ptr fullImage; bool loadedValue = false; bool loadingValue = false; double progressValue = 0.; mutable std::vector thumbnailSizes; mutable std::vector fullSizes; }; class TestChannelRuntime final : public ChannelRuntime { public: [[nodiscard]] bool joinVisible() const override { return joinVisibleValue; } void open(Qt::MouseButton button) const override { openedButtons.push_back(button); } void join(Qt::MouseButton button) const override { joinedButtons.push_back(button); } bool joinVisibleValue = true; mutable std::vector openedButtons; mutable std::vector joinedButtons; }; class TestMediaRuntime final : public MediaRuntime { public: struct InlineBinding { uint64 documentId = 0; std::shared_ptr image; }; struct PhotoBinding { uint64 photoId = 0; std::shared_ptr runtime; }; struct DocumentBinding { uint64 documentId = 0; std::shared_ptr runtime; }; struct MapBinding { double latitude = 0.; double longitude = 0.; uint64 accessHash = 0; int zoom = 0; std::shared_ptr runtime; }; struct ChannelBinding { uint64 channelId = 0; QString username; std::shared_ptr runtime; }; struct MapRequest { double latitude = 0.; double longitude = 0.; uint64 accessHash = 0; QSize size; int zoom = 0; }; struct ChannelRequest { uint64 channelId = 0; QString username; }; [[nodiscard]] std::shared_ptr resolveInlineImage( uint64 documentId, QSize size) const override { inlineRequests.push_back({ documentId, size }); const auto i = std::find_if( inlineBindings.begin(), inlineBindings.end(), [=](const InlineBinding &binding) { return binding.documentId == documentId; }); return (i != inlineBindings.end()) ? i->image : nullptr; } [[nodiscard]] std::shared_ptr resolvePhoto( uint64 photoId) const override { photoRequests.push_back(photoId); const auto i = std::find_if( photoBindings.begin(), photoBindings.end(), [=](const PhotoBinding &binding) { return binding.photoId == photoId; }); return (i != photoBindings.end()) ? i->runtime : nullptr; } [[nodiscard]] std::shared_ptr resolveDocument( uint64 documentId) const override { documentRequests.push_back(documentId); const auto i = std::find_if( documentBindings.begin(), documentBindings.end(), [=](const DocumentBinding &binding) { return binding.documentId == documentId; }); return (i != documentBindings.end()) ? i->runtime : nullptr; } [[nodiscard]] std::shared_ptr resolveMap( double latitude, double longitude, uint64 accessHash, QSize size, int zoom) const override { mapRequests.push_back({ .latitude = latitude, .longitude = longitude, .accessHash = accessHash, .size = size, .zoom = zoom, }); const auto i = std::find_if( mapBindings.begin(), mapBindings.end(), [=](const MapBinding &binding) { return binding.latitude == latitude && binding.longitude == longitude && binding.accessHash == accessHash && binding.zoom == zoom; }); return (i != mapBindings.end()) ? i->runtime : nullptr; } [[nodiscard]] std::shared_ptr resolveChannel( uint64 channelId, const QString &username) const override { channelRequests.push_back({ .channelId = channelId, .username = username, }); const auto i = std::find_if( channelBindings.begin(), channelBindings.end(), [=](const ChannelBinding &binding) { return binding.channelId == channelId && binding.username == username; }); return (i != channelBindings.end()) ? i->runtime : nullptr; } [[nodiscard]] rpl::producer channelJoinedChanges() const override { return _channelJoinedChanges.events(); } void addInlineImage(uint64 documentId, std::shared_ptr image) { inlineBindings.push_back({ .documentId = documentId, .image = std::move(image), }); } void addPhotoRuntime(uint64 photoId, std::shared_ptr runtime) { photoBindings.push_back({ .photoId = photoId, .runtime = std::move(runtime), }); } void addDocumentRuntime( uint64 documentId, std::shared_ptr runtime) { documentBindings.push_back({ .documentId = documentId, .runtime = std::move(runtime), }); } void addMapRuntime( double latitude, double longitude, uint64 accessHash, int zoom, std::shared_ptr runtime) { mapBindings.push_back({ .latitude = latitude, .longitude = longitude, .accessHash = accessHash, .zoom = zoom, .runtime = std::move(runtime), }); } void addChannelRuntime( uint64 channelId, QString username, std::shared_ptr runtime) { channelBindings.push_back({ .channelId = channelId, .username = std::move(username), .runtime = std::move(runtime), }); } void fireChannelJoinedChange(uint64 channelId) const { _channelJoinedChanges.fire_copy(channelId); } std::vector inlineBindings; std::vector photoBindings; std::vector documentBindings; std::vector mapBindings; std::vector channelBindings; mutable std::vector> inlineRequests; mutable std::vector photoRequests; mutable std::vector documentRequests; mutable std::vector mapRequests; mutable std::vector channelRequests; private: mutable rpl::event_stream _channelJoinedChanges; }; enum class NativeIvPlaceholderKind { Video, Embed, EmbedPost, Collage, Slideshow, Audio, Map, }; struct NativeIvPlaceholderFixture { NativeIvPlaceholderKind kind = NativeIvPlaceholderKind::Video; QString caption; QString expectedLabel; MTPPageBlock block; }; struct NativeIvMediaFixture { QString label; QString caption; MTPPageBlock block; QVector photos; QVector documents; }; [[nodiscard]] QImage SolidTestImage( int width, int height, const QColor &color) { auto result = QImage( QSize(std::max(width, 1), std::max(height, 1)), QImage::Format_ARGB32_Premultiplied); result.fill(color); return result; } [[nodiscard]] MTPRichText NativeIvText(QString text) { return text.isEmpty() ? MTP_textEmpty() : MTP_textPlain(MTP_string(text)); } [[nodiscard]] MTPRichText NativeIvConcat( std::initializer_list parts) { auto list = QVector(); list.reserve(int(parts.size())); for (const auto &part : parts) { list.push_back(part); } return MTP_textConcat(MTP_vector(std::move(list))); } [[nodiscard]] MTPRichText NativeIvTextUrl( QString text, QString url, int64 webpageId = 0) { return MTP_textUrl( NativeIvText(std::move(text)), MTP_string(url), MTP_long(webpageId)); } [[nodiscard]] MTPRichText NativeIvTextEmail(QString text, QString email) { return MTP_textEmail( NativeIvText(std::move(text)), MTP_string(email)); } [[nodiscard]] MTPPageCaption NativeIvCaption( QString text = QString(), QString credit = QString()) { return MTP_pageCaption( NativeIvText(std::move(text)), NativeIvText(std::move(credit))); } [[nodiscard]] MTPPhoto NativeIvPhoto(uint64 id, int width, int height) { auto sizes = QVector(); sizes.push_back(MTP_photoSize( MTP_string("y"), MTP_int(width), MTP_int(height), MTP_int(0))); return MTP_photo( MTP_flags(0), MTP_long(id), MTP_long(0), MTP_bytes(), MTP_int(0), MTP_vector(std::move(sizes)), MTPVector(), MTP_int(0)); } [[nodiscard]] MTPPageBlock NativeIvPhotoBlock( uint64 photoId, QString caption = QString(), QString credit = QString(), QString urlOverride = QString()) { const auto flags = urlOverride.isEmpty() ? MTP_flags(0) : MTP_flags(MTPDpageBlockPhoto::Flag::f_url); return MTP_pageBlockPhoto( flags, MTP_long(photoId), NativeIvCaption(std::move(caption), std::move(credit)), MTP_string(urlOverride), MTP_long(0)); } [[nodiscard]] MTPPageBlock NativeIvCoveredPhotoBlock( uint64 photoId, QString caption = QString(), QString credit = QString(), QString urlOverride = QString()) { return MTP_pageBlockCover(NativeIvPhotoBlock( photoId, std::move(caption), std::move(credit), std::move(urlOverride))); } [[nodiscard]] MTPDocument NativeIvDocument( uint64 id, QString mimeType, QVector attributes) { return MTP_document( MTP_flags(0), MTP_long(id), MTP_long(id + 1000), MTP_bytes(), MTP_int(0), MTP_string(mimeType), MTP_long(0), MTPVector(), MTPVector(), MTP_int(0), MTP_vector(std::move(attributes))); } [[nodiscard]] MTPDocument NativeIvVideoDocument( uint64 id, int width, int height, QString fileName = u"video.mp4"_q, double duration = 0.) { const auto flags = MTPDdocumentAttributeVideo::Flags( MTPDdocumentAttributeVideo::Flag::f_supports_streaming); auto attributes = QVector(); attributes.push_back(MTP_documentAttributeFilename(MTP_string(fileName))); attributes.push_back(MTP_documentAttributeVideo( MTP_flags(flags), MTP_double(duration), MTP_int(width), MTP_int(height), MTPint(), MTPdouble(), MTPstring())); return NativeIvDocument(id, u"video/mp4"_q, std::move(attributes)); } [[nodiscard]] MTPDocument NativeIvImageDocument( uint64 id, int width, int height, QString fileName = u"image.jpg"_q) { auto attributes = QVector(); attributes.push_back(MTP_documentAttributeFilename(MTP_string(fileName))); attributes.push_back(MTP_documentAttributeImageSize( MTP_int(width), MTP_int(height))); return NativeIvDocument(id, u"image/jpeg"_q, std::move(attributes)); } [[nodiscard]] MTPDocument NativeIvAudioDocument( uint64 id, QString title, QString performer, QString fileName, int duration) { auto flags = MTPDdocumentAttributeAudio::Flags(0); if (!title.isEmpty()) { flags |= MTPDdocumentAttributeAudio::Flag::f_title; } if (!performer.isEmpty()) { flags |= MTPDdocumentAttributeAudio::Flag::f_performer; } auto attributes = QVector(); attributes.push_back(MTP_documentAttributeFilename(MTP_string(fileName))); attributes.push_back(MTP_documentAttributeAudio( MTP_flags(flags), MTP_int(duration), MTP_string(title), MTP_string(performer), MTPbytes())); return NativeIvDocument(id, u"audio/mpeg"_q, std::move(attributes)); } [[nodiscard]] MTPGeoPoint NativeIvGeoPoint( double latitude, double longitude, uint64 accessHash) { return MTP_geoPoint( MTP_flags(0), MTP_double(longitude), MTP_double(latitude), MTP_long(accessHash), MTPint()); } [[nodiscard]] MTPChat NativeIvChannelChat( uint64 id, QString title, QString username = QString(), uint64 accessHash = 0x12345678ULL) { auto flags = MTPDchannel::Flags( MTPDchannel::Flag::f_broadcast | MTPDchannel::Flag::f_access_hash); if (!username.isEmpty()) { flags |= MTPDchannel::Flag::f_username; } return MTP_channel( MTP_flags(flags), MTP_long(id), MTP_long(accessHash), MTP_string(title), MTP_string(username), MTP_chatPhotoEmpty(), MTP_int(0), MTPVector(), MTPChatAdminRights(), MTPChatBannedRights(), MTPChatBannedRights(), MTPint(), MTPVector(), MTPrecentStory(), MTPPeerColor(), MTPPeerColor(), MTPEmojiStatus(), MTPint(), MTPint(), MTPlong(), MTPlong(), MTPlong()); } [[nodiscard]] NativeIvMediaFixture NativeIvVideoFixture() { return { .label = u"Video"_q, .caption = u"Video caption"_q, .block = MTP_pageBlockVideo( MTP_flags(0), MTP_long(7001), NativeIvCaption(u"Video caption"_q)), .documents = { NativeIvVideoDocument(7001, 1280, 720, u"video.mp4"_q, 42.), }, }; } [[nodiscard]] NativeIvMediaFixture NativeIvAudioFixture() { return { .label = u"Audio"_q, .caption = u"Audio caption"_q, .block = MTP_pageBlockAudio( MTP_long(7002), NativeIvCaption(u"Audio caption"_q)), .documents = { NativeIvAudioDocument( 7002, u"Song Title"_q, u"Sample Artist"_q, u"track.mp3"_q, 215), }, }; } [[nodiscard]] NativeIvMediaFixture NativeIvMapFixture() { return { .label = u"Map"_q, .caption = u"Map caption"_q, .block = MTP_pageBlockMap( NativeIvGeoPoint(51.5007, -0.1246, 880088), MTP_int(13), MTP_int(320), MTP_int(180), NativeIvCaption(u"Map caption"_q)), }; } [[nodiscard]] NativeIvMediaFixture NativeIvChannelFixture() { return { .label = u"Channel"_q, .block = MTP_pageBlockChannel(NativeIvChannelChat( 7006, u"Native IV Channel"_q, u"nativeiv"_q)), }; } [[nodiscard]] NativeIvMediaFixture NativeIvCollageFixture() { auto items = QVector(); items.push_back(NativeIvPhotoBlock(9102)); items.push_back(MTP_pageBlockVideo( MTP_flags(0), MTP_long(7003), NativeIvCaption())); return { .label = u"Collage"_q, .caption = u"Collage caption"_q, .block = MTP_pageBlockCollage( MTP_vector(std::move(items)), NativeIvCaption(u"Collage caption"_q)), .photos = { NativeIvPhoto(9102, 400, 300), }, .documents = { NativeIvVideoDocument(7003, 640, 360, u"collage.mp4"_q, 13.), }, }; } [[nodiscard]] NativeIvMediaFixture NativeIvSlideshowFixture() { auto items = QVector(); items.push_back(MTP_pageBlockVideo( MTP_flags(0), MTP_long(7004), NativeIvCaption())); items.push_back(MTP_pageBlockVideo( MTP_flags(0), MTP_long(7005), NativeIvCaption())); return { .label = u"Slideshow"_q, .caption = u"Slideshow caption"_q, .block = MTP_pageBlockSlideshow( MTP_vector(std::move(items)), NativeIvCaption(u"Slideshow caption"_q)), .documents = { NativeIvVideoDocument(7004, 960, 540, u"slide-a.mp4"_q, 11.), NativeIvVideoDocument(7005, 854, 480, u"slide-b.mp4"_q, 9.), }, }; } [[nodiscard]] QString NativeIvPlaceholderLabel(NativeIvPlaceholderKind kind) { switch (kind) { case NativeIvPlaceholderKind::Video: return u"Video Placeholder"_q; case NativeIvPlaceholderKind::Embed: case NativeIvPlaceholderKind::EmbedPost: return u"Embed Placeholder"_q; case NativeIvPlaceholderKind::Collage: return u"Collage placeholder"_q; case NativeIvPlaceholderKind::Slideshow: return u"Grouped Media Placeholder"_q; case NativeIvPlaceholderKind::Audio: return u"Audio File Placeholder"_q; case NativeIvPlaceholderKind::Map: return u"Map Placeholder"_q; } return QString(); } [[nodiscard]] MTPPageBlock NativeIvPlaceholderBlock( NativeIvPlaceholderKind kind, QString caption = QString()) { const auto pageCaption = NativeIvCaption(std::move(caption)); switch (kind) { case NativeIvPlaceholderKind::Video: return MTP_pageBlockVideo( MTP_flags(0), MTP_long(7001), pageCaption); case NativeIvPlaceholderKind::Embed: return MTP_pageBlockEmbed( MTP_flags(0), MTP_string(), MTP_string(), MTP_long(0), MTP_int(0), MTP_int(0), pageCaption); case NativeIvPlaceholderKind::EmbedPost: return MTP_pageBlockEmbedPost( MTP_string("https://example.com/embed-post"), MTP_long(0), MTP_long(0), MTP_string("Author"), MTP_int(0), MTP_vector(), pageCaption); case NativeIvPlaceholderKind::Collage: return MTP_pageBlockCollage( MTP_vector(), pageCaption); case NativeIvPlaceholderKind::Slideshow: return MTP_pageBlockSlideshow( MTP_vector(), pageCaption); case NativeIvPlaceholderKind::Audio: return MTP_pageBlockAudio(MTP_long(7002), pageCaption); case NativeIvPlaceholderKind::Map: return MTP_pageBlockMap( MTP_geoPointEmpty(), MTP_int(10), MTP_int(120), MTP_int(72), pageCaption); } return MTP_pageBlockUnsupported(); } [[nodiscard]] MTPRichText NativeIvInlineImageText( uint64 documentId, int width, int height) { return MTP_textImage( MTP_long(documentId), MTP_int(width), MTP_int(height)); } [[nodiscard]] MTPPageBlock NativeIvInlineImageParagraph( uint64 documentId, int width, int height, QString prefix, QString suffix) { return MTP_pageBlockParagraph(NativeIvConcat({ NativeIvText(std::move(prefix)), NativeIvInlineImageText(documentId, width, height), NativeIvText(std::move(suffix)), })); } [[nodiscard]] Iv::Source NativeIvSource( QVector blocks, QVector photos = {}, QVector documents = {}) { auto result = Iv::Source(); result.pageId = 1; result.page = MTP_page( MTP_flags(0), MTP_string("https://example.com/native-iv"), MTP_vector(std::move(blocks)), MTP_vector(std::move(photos)), MTP_vector(std::move(documents)), MTP_int(0)); result.name = u"native-iv-test"_q; return result; } [[nodiscard]] QString FromLatin1(const char *value) { return QString::fromLatin1(value); } void PrintStreamLine(std::ostream &stream, const QString &line) { const auto bytes = line.toUtf8(); stream.write(bytes.constData(), static_cast(bytes.size())); stream << '\n'; } void PrintLine(const QString &line) { PrintStreamLine(std::cout, line); } void PrintError(const QString &line) { PrintStreamLine(std::cerr, line); } [[nodiscard]] Args ParseArgs(int argc, char **argv) { auto result = Args(); for (auto i = 1; i != argc; ++i) { const auto argument = QString::fromLocal8Bit(argv[i]); if (argument == FromLatin1("--dump")) { result.dump = true; } else if (argument == FromLatin1("--inline-html")) { result.inlineHtml = true; } else if (argument == FromLatin1("--markdown") || argument == FromLatin1("--latex-md")) { if (i + 1 == argc) { result.ok = false; result.error = FromLatin1("missing value for ") + argument; return result; } const auto path = QString::fromLocal8Bit(argv[++i]); if (argument == FromLatin1("--markdown")) { result.markdownPath = path; } else { result.latexMarkdownPath = path; } } else { result.ok = false; result.error = FromLatin1("unknown argument: ") + argument; return result; } } return result; } [[nodiscard]] QString DefaultFixturePath(const QString &name) { const auto applicationDir = QDir(QCoreApplication::applicationDirPath()); const auto applicationCandidate = applicationDir.filePath(name); if (QFileInfo::exists(applicationCandidate)) { return applicationCandidate; } const auto outDebug = QDir::current().filePath( FromLatin1("out/Debug/") + name); if (QFileInfo::exists(outDebug)) { return outDebug; } const auto repoFixtureFromApplication = QDir::cleanPath( applicationDir.filePath( FromLatin1( "../../Telegram/SourceFiles/tests/fixtures/markdown_iv/") + name)); if (QFileInfo::exists(repoFixtureFromApplication)) { return repoFixtureFromApplication; } const auto repoFixtureFromCurrent = QDir::current().filePath( FromLatin1("Telegram/SourceFiles/tests/fixtures/markdown_iv/") + name); if (QFileInfo::exists(repoFixtureFromCurrent)) { return repoFixtureFromCurrent; } return outDebug; } [[nodiscard]] bool ReadFile(const QString &path, QByteArray *bytes) { if (!bytes) { return false; } auto file = QFile(path); if (!file.open(QIODevice::ReadOnly)) { return false; } *bytes = file.readAll(); return true; } [[nodiscard]] int CountNodes(const MarkdownNode &node) { auto result = 1; for (const auto &child : node.children) { result += CountNodes(child); } return result; } [[nodiscard]] bool HasKind(const MarkdownNode &node, NodeKind kind) { if (node.kind == kind) { return true; } for (const auto &child : node.children) { if (HasKind(child, kind)) { return true; } } return false; } [[nodiscard]] bool HasTextContaining( const MarkdownNode &node, const QString &text) { if (node.text.contains(text) || node.raw.contains(text)) { return true; } for (const auto &child : node.children) { if (HasTextContaining(child, text)) { return true; } } return false; } [[nodiscard]] bool HasExactInlineHtmlTriplet( const MarkdownNode &node, const QString &openingTag, const QString &innerText, const QString &closingTag) { const auto count = int(node.children.size()); for (auto i = 0; (i + 2) < count; ++i) { const auto &opening = node.children[i]; const auto &text = node.children[i + 1]; const auto &closing = node.children[i + 2]; if (opening.kind == NodeKind::HtmlInline && opening.raw == openingTag && text.kind == NodeKind::Text && text.text == innerText && closing.kind == NodeKind::HtmlInline && closing.raw == closingTag) { return true; } } for (const auto &child : node.children) { if (HasExactInlineHtmlTriplet( child, openingTag, innerText, closingTag)) { return true; } } return false; } [[nodiscard]] bool HasTaskState(const MarkdownNode &node, TaskState state) { if (node.taskState == state) { return true; } for (const auto &child : node.children) { if (HasTaskState(child, state)) { return true; } } return false; } void CollectTables( const MarkdownNode &node, std::vector *out) { if (!out) { return; } if (node.kind == NodeKind::Table) { out->push_back(&node); } for (const auto &child : node.children) { CollectTables(child, out); } } [[nodiscard]] int TableHeaderRowCount(const MarkdownNode &table) { auto result = 0; for (const auto &row : table.children) { if (row.kind == NodeKind::TableRow && row.tableHeader) { ++result; } } return result; } [[nodiscard]] bool HasTableHeaderRow(const MarkdownNode &table) { return TableHeaderRowCount(table) > 0; } [[nodiscard]] bool HasSequentialTableColumns(const MarkdownNode &table) { for (const auto &row : table.children) { if (row.kind != NodeKind::TableRow) { return false; } auto expectedColumn = 0; for (const auto &cell : row.children) { if (cell.kind != NodeKind::TableCell || cell.tableColumn != expectedColumn) { return false; } ++expectedColumn; } } return !table.children.empty(); } [[nodiscard]] bool HasTableAlignments( const MarkdownNode &table, const std::vector &expected) { if (table.tableAlignments.size() != expected.size()) { return false; } for (auto i = 0, count = int(expected.size()); i != count; ++i) { if (table.tableAlignments[i] != expected[i]) { return false; } } return true; } [[nodiscard]] int TableColumnCount(const MarkdownNode &table) { auto result = 0; for (const auto &row : table.children) { if (row.kind != NodeKind::TableRow) { return 0; } const auto count = int(row.children.size()); if (count > result) { result = count; } } return result; } [[nodiscard]] int CountDisplayMathNodes(const MarkdownNode &node) { auto result = (node.kind == NodeKind::DisplayMath) ? 1 : 0; for (const auto &child : node.children) { result += CountDisplayMathNodes(child); } return result; } [[nodiscard]] bool SameRange( const SourceRange &range, int startLine, int startColumn, int endLine, int endColumn) { return range.available && (range.startLine == startLine) && (range.startColumn == startColumn) && (range.endLine == endLine) && (range.endColumn == endColumn); } [[nodiscard]] bool CoversLineRange( const SourceRange &range, int firstLine, int lastLine) { return range.available && (range.startLine <= firstLine) && (range.endLine >= lastLine); } [[nodiscard]] const MarkdownNode *FindNodeByKindAndRange( const MarkdownNode &node, NodeKind kind, int startLine, int startColumn, int endLine, int endColumn) { if (node.kind == kind && SameRange( node.range, startLine, startColumn, endLine, endColumn)) { return &node; } for (const auto &child : node.children) { if (const auto found = FindNodeByKindAndRange( child, kind, startLine, startColumn, endLine, endColumn)) { return found; } } return nullptr; } [[nodiscard]] int CountNodesByKindAndRange( const MarkdownNode &node, NodeKind kind, int startLine, int startColumn, int endLine, int endColumn) { auto result = (node.kind == kind && SameRange( node.range, startLine, startColumn, endLine, endColumn)) ? 1 : 0; for (const auto &child : node.children) { result += CountNodesByKindAndRange( child, kind, startLine, startColumn, endLine, endColumn); } return result; } using NodeKindPath = std::initializer_list; using NodeKindPathIter = NodeKindPath::const_iterator; [[nodiscard]] const MarkdownNode *FindNodeByPathAndRange( const MarkdownNode &node, NodeKindPathIter begin, NodeKindPathIter end, int startLine, int startColumn, int endLine, int endColumn) { if (begin == end || node.kind != *begin) { return nullptr; } if (std::next(begin) == end) { return SameRange( node.range, startLine, startColumn, endLine, endColumn) ? &node : nullptr; } const auto next = std::next(begin); for (const auto &child : node.children) { if (const auto found = FindNodeByPathAndRange( child, next, end, startLine, startColumn, endLine, endColumn)) { return found; } } return nullptr; } [[nodiscard]] const MarkdownNode *FindNodeByPathAndRange( const MarkdownNode &node, NodeKindPath path, int startLine, int startColumn, int endLine, int endColumn) { return path.size() ? FindNodeByPathAndRange( node, path.begin(), path.end(), startLine, startColumn, endLine, endColumn) : nullptr; } [[nodiscard]] const MarkdownNode *FindNodeByKindAndLineRange( const MarkdownNode &node, NodeKind kind, int firstLine, int lastLine) { if (node.kind == kind && CoversLineRange(node.range, firstLine, lastLine)) { return &node; } for (const auto &child : node.children) { if (const auto found = FindNodeByKindAndLineRange( child, kind, firstLine, lastLine)) { return found; } } return nullptr; } [[nodiscard]] const MarkdownNode *FindHtmlInlineByRaw( const MarkdownNode &node, const QString &raw) { if (node.kind == NodeKind::HtmlInline && node.raw == raw) { return &node; } for (const auto &child : node.children) { if (const auto found = FindHtmlInlineByRaw(child, raw)) { return found; } } return nullptr; } [[nodiscard]] const MarkdownNode *FindHtmlBlockContaining( const MarkdownNode &node, const QString &text) { if (node.kind == NodeKind::HtmlBlock && node.raw.contains(text)) { return &node; } for (const auto &child : node.children) { if (const auto found = FindHtmlBlockContaining(child, text)) { return found; } } return nullptr; } void CollectNodesByKind( const MarkdownNode &node, NodeKind kind, std::vector *out) { if (!out) { return; } if (node.kind == kind) { out->push_back(&node); } for (const auto &child : node.children) { CollectNodesByKind(child, kind, out); } } [[nodiscard]] const MarkdownNode *FindLinkByTarget( const MarkdownNode &node, const QString &target) { if (node.kind == NodeKind::Link && node.url == target) { return &node; } for (const auto &child : node.children) { if (const auto found = FindLinkByTarget(child, target)) { return found; } } return nullptr; } [[nodiscard]] bool WarningContains( const PreparedDocument &document, const QString &snippet) { for (const auto &warning : document.warnings) { if (warning.contains(snippet, Qt::CaseInsensitive)) { return true; } } return false; } [[nodiscard]] int CountFormulas( const PreparedDocument &document, MathKind kind) { auto result = 0; for (const auto &formula : document.formulas) { if (formula.kind == kind) { ++result; } } return result; } [[nodiscard]] bool HasFormulaOnLine( const PreparedDocument &document, int line, const QString &tex) { for (const auto &formula : document.formulas) { if (formula.range.available && formula.range.startLine == line && formula.tex == tex) { return true; } } return false; } [[nodiscard]] bool HasFormulaInLineRange( const PreparedDocument &document, int firstLine, int lastLine) { if (lastLine < firstLine) { return false; } for (const auto &formula : document.formulas) { if (!formula.range.available) { continue; } if (formula.range.startLine <= lastLine && formula.range.endLine >= firstLine) { return true; } } return false; } [[nodiscard]] int CountFormulasInLineRange( const PreparedDocument &document, MathKind kind, int firstLine, int lastLine) { auto result = 0; for (const auto &formula : document.formulas) { if (!formula.range.available || formula.kind != kind) { continue; } if (formula.range.startLine <= lastLine && formula.range.endLine >= firstLine) { ++result; } } return result; } [[nodiscard]] QString YesNo(bool value) { return FromLatin1(value ? "yes" : "no"); } [[nodiscard]] bool HasBothTaskStates(const PreparedDocument &document) { return HasTaskState(document.document, TaskState::Checked) && HasTaskState(document.document, TaskState::Unchecked); } [[nodiscard]] bool ExclusionsPass(const PreparedDocument &document) { return !HasFormulaInLineRange(document, 281, 281) && HasFormulaOnLine(document, 285, FromLatin1("5x + 3")) && !HasFormulaInLineRange(document, 332, 340); } [[nodiscard]] bool HasFormula( const PreparedDocument &document, MathKind kind, const QString &tex) { for (const auto &formula : document.formulas) { if (formula.kind == kind && formula.tex == tex) { return true; } } return false; } void Check(bool condition, const QString &message, bool *ok); [[nodiscard]] MarkdownSourceValidationResult CheckValidationSuccess( const QByteArray &source, const QString &label, bool *ok) { auto validated = ValidateMarkdownSourceForIv(source, ParseOptions{ label }); Check( validated.ok, label + FromLatin1(" validation failed: ") + validated.error, ok); return validated; } void CheckValidationFailure( const QByteArray &source, const QString &label, const QString &expectedError, bool *ok) { const auto validated = ValidateMarkdownSourceForIv( source, ParseOptions{ label }); Check( !validated.ok, label + FromLatin1(" validation should fail"), ok); if (!validated.ok) { Check( validated.error == expectedError, label + FromLatin1(" validation error should be ") + expectedError + FromLatin1(", got ") + validated.error, ok); } } void CheckMatchingParseCounts( const PreparedDocument &legacy, const PreparedDocument &validated, const QString &label, bool *ok) { Check( legacy.stats.cmarkNodeCount == validated.stats.cmarkNodeCount, label + FromLatin1(" validated path cmark node count"), ok); Check( CountNodes(legacy.document) == CountNodes(validated.document), label + FromLatin1(" validated path converted node count"), ok); Check( legacy.formulas.size() == validated.formulas.size(), label + FromLatin1(" validated path formula count"), ok); Check( CountFormulas(legacy, MathKind::Inline) == CountFormulas(validated, MathKind::Inline), label + FromLatin1(" validated path inline formula count"), ok); Check( CountFormulas(legacy, MathKind::Display) == CountFormulas(validated, MathKind::Display), label + FromLatin1(" validated path display formula count"), ok); Check( CountDisplayMathNodes(legacy.document) == CountDisplayMathNodes(validated.document), label + FromLatin1(" validated path display math node count"), ok); } void AppendSummaryCounts(QString *line, const PreparedDocument &document) { line->append(FromLatin1(" nodes=")); line->append(QString::number(document.stats.cmarkNodeCount)); line->append(FromLatin1(" converted=")); line->append(QString::number(CountNodes(document.document))); line->append(FromLatin1(" formulas_inline=")); line->append(QString::number(CountFormulas(document, MathKind::Inline))); line->append(FromLatin1(" formulas_display=")); line->append(QString::number(CountFormulas(document, MathKind::Display))); } void PrintSummary(const PreparedDocument &document, const QString &label) { auto line = label; AppendSummaryCounts(&line, document); line.append(FromLatin1(" tables=")); line.append(YesNo(HasKind(document.document, NodeKind::Table))); if (label == FromLatin1("markdown-example.md")) { line.append(FromLatin1(" tasks=")); line.append(YesNo(HasBothTaskStates(document))); line.append(FromLatin1(" strike=")); line.append(YesNo(HasKind(document.document, NodeKind::Strike))); } else if (label == FromLatin1("latex-markdown-test.md")) { line.append(FromLatin1(" exclusions=")); line.append(YesNo(ExclusionsPass(document))); } PrintLine(line); } [[nodiscard]] bool ParseFixture( const QString &path, const QString &label, PreparedDocument *document) { auto bytes = QByteArray(); if (!ReadFile(path, &bytes)) { PrintError(label + FromLatin1(" read-failed: ") + path); return false; } auto parsed = ParseMarkdownForIv(bytes, ParseOptions{ label }); if (!parsed.ok) { PrintError(label + FromLatin1(" parse-failed: ") + parsed.error); return false; } auto validated = ValidateMarkdownSourceForIv( bytes, ParseOptions{ label }); if (!validated.ok) { PrintError(label + FromLatin1(" validate-failed: ") + validated.error); return false; } auto parsedValidated = ParseMarkdownForIv(std::move(validated.source)); if (!parsedValidated.ok) { PrintError( label + FromLatin1(" validated-parse-failed: ") + parsedValidated.error); return false; } auto countsOk = true; CheckMatchingParseCounts( parsed.document, parsedValidated.document, label, &countsOk); if (!countsOk) { return false; } PrintSummary(parsed.document, label); if (document) { *document = std::move(parsed.document); } return true; } [[nodiscard]] QString AbsolutePath(const QString &path) { return QDir::cleanPath(QFileInfo(path).absoluteFilePath()); } [[nodiscard]] QString PrepareFailureReason( const PrepareFailureStatus &failure) { return !failure.debugReason.isEmpty() ? failure.debugReason : QString::number(int(failure.terminal)); } [[nodiscard]] MarkdownArticleContent PrepareParsedDocumentForTest( std::shared_ptr document, const QString &sourcePath, const std::shared_ptr &renderer, MarkdownPrepareDimensions dimensions = CaptureMarkdownPrepareDimensions()) { return PrepareSynchronously({ .document = std::move(document), .renderer = renderer, .dimensions = std::move(dimensions), .sourcePath = AbsolutePath(sourcePath), }); } [[nodiscard]] MarkdownArticleContent PrepareParsedDocumentForTest( const PreparedDocument &document, const QString &sourcePath, const std::shared_ptr &renderer, MarkdownPrepareDimensions dimensions = CaptureMarkdownPrepareDimensions()) { return PrepareParsedDocumentForTest( std::make_shared(document), sourcePath, renderer, std::move(dimensions)); } [[nodiscard]] int CountPreparedFormulaSlots( const MarkdownArticleContent &prepared) { auto result = 0; for (const auto &slot : prepared.formulas) { if (slot.present) { ++result; } } return result; } [[nodiscard]] int CountMeasuredFormulaSlots( const MarkdownArticleContent &prepared) { auto result = 0; for (const auto &slot : prepared.formulas) { if (slot.present && slot.measured.success) { ++result; } } return result; } void PrintPrepareSummary( const QString &label, const MarkdownArticleContent &prepared) { auto line = label; line.append(FromLatin1(" prepare_ms=")); line.append(QString::number(prepared.debug.prepareMs)); line.append(FromLatin1(" formula_measure_ms=")); line.append(QString::number(prepared.debug.formulaMeasureMs)); line.append(FromLatin1(" formula_render_ms=")); line.append(QString::number(prepared.debug.formulaRenderMs)); line.append(FromLatin1(" prepare_warnings=")); line.append(QString::number(prepared.debug.prepareWarningCount)); line.append(FromLatin1(" formula_warnings=")); line.append(QString::number(prepared.debug.formulaWarningCount)); line.append(FromLatin1(" prepared_formulas=")); line.append(QString::number(CountPreparedFormulaSlots(prepared))); PrintLine(line); } [[nodiscard]] bool PrepareFixture( const QString &path, const QString &label, const std::shared_ptr &renderer, PreparedFixture *fixture) { auto parsed = PreparedDocument(); if (!ParseFixture(path, label, &parsed)) { return false; } auto prepared = PrepareParsedDocumentForTest(parsed, path, renderer); if (prepared.failure.failed()) { PrintError( label + FromLatin1(" prepare-failed: ") + PrepareFailureReason(prepared.failure)); return false; } PrintPrepareSummary(label, prepared); if (fixture) { fixture->label = label; fixture->path = AbsolutePath(path); fixture->parsed = std::move(parsed); fixture->prepared = std::move(prepared); } return true; } [[nodiscard]] std::unique_ptr BuildArticleForTest( MarkdownArticleContent content, const std::shared_ptr &renderer, int width, int *height = nullptr) { auto article = std::make_unique(renderer); article->setContent(std::move(content)); if (height) { *height = article->resizeGetHeight(width); } else { const auto computed = article->resizeGetHeight(width); (void)computed; } return article; } [[nodiscard]] QImage PaintArticleForTest( MarkdownArticle *article, int width, int height, int devicePixelRatio = 1, std::optional selection = std::nullopt) { const auto previousDevicePixelRatio = style::DevicePixelRatio(); style::SetDevicePixelRatio(devicePixelRatio); auto image = QImage( QSize(width * devicePixelRatio, height * devicePixelRatio), QImage::Format_ARGB32_Premultiplied); image.setDevicePixelRatio(devicePixelRatio); image.fill(Qt::transparent); { auto painter = Painter(&image); article->paint( painter, QRect(0, 0, width, height), MarkdownArticlePaintCaches(), selection.value_or(MarkdownArticleSelection())); } style::SetDevicePixelRatio(previousDevicePixelRatio); return image; } [[nodiscard]] QImage PaintArticleForSyntaxHighlightTest( MarkdownArticle *article, int width, int height, int devicePixelRatio = 1) { static auto preCache = [] { auto result = Ui::Text::QuotePaintCache(); const auto color = st::defaultMarkdownTextPalette.monoFg->c; result.bg = color; result.bg.setAlpha(Ui::kDefaultBgOpacity * 255); result.outlines[0] = color; result.outlines[0].setAlpha(Ui::kDefaultOutline1Opacity * 255); result.outlines[1] = result.outlines[2] = QColor(0, 0, 0, 0); result.header = color; result.header.setAlpha(Ui::kDefaultOutline2Opacity * 255); result.icon = color; result.icon.setAlpha(Ui::kDefaultOutline3Opacity * 255); return result; }(); static auto highlightColors = Ui::SyntaxHighlightColors( style::main_palette::get()); const auto previousDevicePixelRatio = style::DevicePixelRatio(); style::SetDevicePixelRatio(devicePixelRatio); auto image = QImage( QSize(width * devicePixelRatio, height * devicePixelRatio), QImage::Format_ARGB32_Premultiplied); image.setDevicePixelRatio(devicePixelRatio); image.fill(Qt::transparent); { auto painter = Painter(&image); article->paint( painter, QRect(0, 0, width, height), MarkdownArticlePaintCaches{ .pre = &preCache, .colors = highlightColors, }); } style::SetDevicePixelRatio(previousDevicePixelRatio); return image; } [[nodiscard]] QImage RenderWidgetForTest(QWidget *widget) { if (!widget || widget->size().isEmpty()) { return QImage(); } auto image = QImage( widget->size(), QImage::Format_ARGB32_Premultiplied); image.fill(Qt::transparent); widget->render(&image); return image; } class MarkdownArticleHighlightWaiter final { public: explicit MarkdownArticleHighlightWaiter(MarkdownArticle *article) : _article(article) { Spellchecker::HighlightReady( ) | rpl::on_next([=](Spellchecker::HighlightProcessId processId) { if (_article && _article->highlightProcessDone(processId)) { _done = true; } }, _lifetime); } [[nodiscard]] bool wait(int timeoutMs = 5000) { auto timer = QElapsedTimer(); timer.start(); while (!_done && (timer.elapsed() < timeoutMs)) { QCoreApplication::processEvents(QEventLoop::AllEvents, 20); } return _done; } private: MarkdownArticle *_article = nullptr; rpl::lifetime _lifetime; bool _done = false; }; [[nodiscard]] bool HasPaintedPixels(const QImage &image) { const auto bits = image.constBits(); if (!bits) { return false; } const auto count = image.sizeInBytes(); for (auto i = qsizetype(0); i != count; ++i) { if (bits[i] != 0) { return true; } } return false; } void FlushPendingWidgetEvents() { for (auto i = 0; i != 3; ++i) { QCoreApplication::sendPostedEvents(); QCoreApplication::processEvents(QEventLoop::AllEvents, 20); } } class WidgetEventCounter final : public QObject { public: void reset() { paintCount = 0; updateRequestCount = 0; } int paintCount = 0; int updateRequestCount = 0; protected: bool eventFilter(QObject *object, QEvent *event) override { Q_UNUSED(object); if (event->type() == QEvent::Paint) { ++paintCount; } else if (event->type() == QEvent::UpdateRequest) { ++updateRequestCount; } return false; } }; template [[nodiscard]] Object *FindChildObject(QObject *root) { if (!root) { return nullptr; } for (auto child : root->children()) { if (const auto object = dynamic_cast(child)) { return object; } if (const auto nested = FindChildObject(child)) { return nested; } } return nullptr; } [[nodiscard]] bool PaintTouchesBottomOrRightImageEdge(const QImage &image) { if (image.isNull()) { return false; } const auto width = image.width(); const auto height = image.height(); const auto painted = [&](int x, int y) { return (image.pixel(x, y) & 0xFF000000U) != 0; }; for (auto x = 0; x != width; ++x) { if (painted(x, height - 1)) { return true; } } for (auto y = 0; y != height; ++y) { if (painted(width - 1, y)) { return true; } } return false; } [[nodiscard]] std::optional PaintedBoundsInRect( const QImage &image, QRect rect) { rect = rect.intersected(QRect(QPoint(), image.size())); if (rect.isEmpty()) { return std::nullopt; } auto left = rect.right(); auto top = rect.bottom(); auto right = rect.left() - 1; auto bottom = rect.top() - 1; for (auto y = rect.top(); y <= rect.bottom(); ++y) { for (auto x = rect.left(); x <= rect.right(); ++x) { if (!(image.pixel(x, y) & 0xFF000000U)) { continue; } left = std::min(left, x); top = std::min(top, y); right = std::max(right, x); bottom = std::max(bottom, y); } } return (right >= left) && (bottom >= top) ? std::make_optional(QRect(QPoint(left, top), QPoint(right, bottom))) : std::nullopt; } [[nodiscard]] bool PixelsDifferInRect( const QImage &first, const QImage &second, QRect rect) { if (first.size() != second.size()) { return false; } rect = rect.intersected(QRect(QPoint(), first.size())); if (rect.isEmpty()) { return false; } for (auto y = rect.top(); y <= rect.bottom(); ++y) { for (auto x = rect.left(); x <= rect.right(); ++x) { if (first.pixel(x, y) != second.pixel(x, y)) { return true; } } } return false; } [[nodiscard]] std::optional ChangedBoundsInRect( const QImage &first, const QImage &second, QRect rect) { if (first.size() != second.size()) { return std::nullopt; } rect = rect.intersected(QRect(QPoint(), first.size())); if (rect.isEmpty()) { return std::nullopt; } auto left = rect.right(); auto top = rect.bottom(); auto right = rect.left() - 1; auto bottom = rect.top() - 1; for (auto y = rect.top(); y <= rect.bottom(); ++y) { for (auto x = rect.left(); x <= rect.right(); ++x) { if (first.pixel(x, y) == second.pixel(x, y)) { continue; } left = std::min(left, x); top = std::min(top, y); right = std::max(right, x); bottom = std::max(bottom, y); } } return (right >= left) && (bottom >= top) ? std::make_optional(QRect(QPoint(left, top), QPoint(right, bottom))) : std::nullopt; } [[nodiscard]] std::optional SymbolHitBounds( MarkdownArticle *article, int width, int height, int offset, int expectedSegmentIndex = -1) { if (!article || (width <= 0) || (height <= 0) || (offset < 0) || (expectedSegmentIndex < -1)) { return std::nullopt; } auto flags = Ui::Text::StateRequest::Flags(); flags |= Ui::Text::StateRequest::Flag::LookupSymbol; auto left = width; auto top = height; auto right = -1; auto bottom = -1; for (auto y = 0; y != height; ++y) { for (auto x = 0; x != width; ++x) { const auto hit = article->hitTest(QPoint(x, y), flags); if (!hit.valid() || !hit.state.uponSymbol || ((expectedSegmentIndex >= 0) && (hit.segmentIndex != expectedSegmentIndex)) || (int(hit.state.symbol) != offset)) { continue; } left = std::min(left, x); top = std::min(top, y); right = std::max(right, x); bottom = std::max(bottom, y); } } return (right >= left) && (bottom >= top) ? std::make_optional(QRect(QPoint(left, top), QPoint(right, bottom))) : std::nullopt; } [[nodiscard]] std::optional SymbolRangeHitBounds( MarkdownArticle *article, int width, int height, int offset, int length, int expectedSegmentIndex = -1) { if (!article || (width <= 0) || (height <= 0) || (offset < 0) || (length <= 0) || (expectedSegmentIndex < -1)) { return std::nullopt; } auto flags = Ui::Text::StateRequest::Flags(); flags |= Ui::Text::StateRequest::Flag::LookupSymbol; auto left = width; auto top = height; auto right = -1; auto bottom = -1; const auto end = offset + length; for (auto y = 0; y != height; ++y) { for (auto x = 0; x != width; ++x) { const auto hit = article->hitTest(QPoint(x, y), flags); if (!hit.valid() || !hit.state.uponSymbol || ((expectedSegmentIndex >= 0) && (hit.segmentIndex != expectedSegmentIndex))) { continue; } const auto symbol = int(hit.state.symbol); if (symbol < offset || symbol >= end) { continue; } left = std::min(left, x); top = std::min(top, y); right = std::max(right, x); bottom = std::max(bottom, y); } } return (right >= left) && (bottom >= top) ? std::make_optional(QRect(QPoint(left, top), QPoint(right, bottom))) : std::nullopt; } [[nodiscard]] std::optional SegmentHitBounds( MarkdownArticle *article, int width, int height, int expectedSegmentIndex) { if (!article || (width <= 0) || (height <= 0) || (expectedSegmentIndex < 0)) { return std::nullopt; } auto flags = Ui::Text::StateRequest::Flags(); flags |= Ui::Text::StateRequest::Flag::LookupSymbol; auto left = width; auto top = height; auto right = -1; auto bottom = -1; for (auto y = 0; y != height; ++y) { for (auto x = 0; x != width; ++x) { const auto hit = article->hitTest(QPoint(x, y), flags); if (!hit.valid() || !hit.direct || (hit.segmentIndex != expectedSegmentIndex)) { continue; } left = std::min(left, x); top = std::min(top, y); right = std::max(right, x); bottom = std::max(bottom, y); } } return (right >= left) && (bottom >= top) ? std::make_optional(QRect(QPoint(left, top), QPoint(right, bottom))) : std::nullopt; } template [[nodiscard]] std::optional HitBoundsWhere( MarkdownArticle *article, int width, int height, Ui::Text::StateRequest::Flags flags, Predicate &&predicate) { if (!article || (width <= 0) || (height <= 0)) { return std::nullopt; } auto left = width; auto top = height; auto right = -1; auto bottom = -1; for (auto y = 0; y != height; ++y) { for (auto x = 0; x != width; ++x) { const auto hit = article->hitTest(QPoint(x, y), flags); if (!hit.valid() || !hit.direct || !predicate(hit)) { continue; } left = std::min(left, x); top = std::min(top, y); right = std::max(right, x); bottom = std::max(bottom, y); } } return (right >= left) && (bottom >= top) ? std::make_optional(QRect(QPoint(left, top), QPoint(right, bottom))) : std::nullopt; } [[nodiscard]] std::vector MeasuredFormulaPointers( const MarkdownArticleContent &prepared) { auto result = std::vector(); for (const auto &slot : prepared.formulas) { if (slot.present && slot.measuredData) { result.push_back(slot.measuredData.get()); } } return result; } [[nodiscard]] const PreparedFormulaSlot *FindPreparedFormulaSlot( const MarkdownArticleContent &prepared, const QString &trimmedTex, MathKind kind) { for (const auto &slot : prepared.formulas) { if (slot.present && slot.kind == kind && slot.trimmedTex == trimmedTex) { return &slot; } } return nullptr; } template void ForEachPreparedBlock( const std::vector &blocks, Callback &&callback) { for (const auto &block : blocks) { callback(block); ForEachPreparedBlock(block.children, callback); } } template void ForEachPreparedLink( const std::vector &blocks, Callback &&callback) { ForEachPreparedBlock(blocks, [&](const PreparedBlock &block) { for (const auto &link : block.links) { callback(link); } for (const auto &row : block.tableRows) { for (const auto &cell : row.cells) { for (const auto &link : cell.links) { callback(link); } } } }); } template [[nodiscard]] const PreparedLink *FindPreparedLink( const std::vector &blocks, Predicate &&predicate) { const PreparedLink *result = nullptr; ForEachPreparedLink(blocks, [&](const PreparedLink &link) { if (!result && predicate(link)) { result = &link; } }); return result; } [[nodiscard]] const PreparedFootnote *FindPreparedFootnote( const MarkdownArticleContent &prepared, const QString &label) { for (const auto &footnote : prepared.footnotes) { if (footnote.label == label) { return &footnote; } } return nullptr; } template void ForEachPreparedFootnoteLink( const MarkdownArticleContent &prepared, Callback &&callback) { for (const auto &footnote : prepared.footnotes) { for (const auto &link : footnote.links) { callback(link); } ForEachPreparedLink(footnote.blocks, callback); } } [[nodiscard]] const PreparedBlock *FindPreparedParagraphContaining( const MarkdownArticleContent &prepared, const QString &text) { const PreparedBlock *result = nullptr; ForEachPreparedBlock(prepared.blocks.blocks, [&](const PreparedBlock &block) { if (!result && block.kind == PreparedBlockKind::Paragraph && block.text.text.contains(text)) { result = █ } }); return result; } [[nodiscard]] bool HasEntityRange( const TextWithEntities &text, EntityType type, int offset, int length) { for (const auto &entity : text.entities) { if (entity.type() == type && entity.offset() == offset && entity.length() == length) { return true; } } return false; } [[nodiscard]] uint16 PreparedLinkIndexFromCustomUrlData( const QString &data) { const auto prefix = FromLatin1("internal:index"); if (!data.startsWith(prefix) || data.size() != prefix.size() + 1) { return 0; } return static_cast(data[prefix.size()].unicode()); } [[nodiscard]] const PreparedLink *FindPreparedLinkByCustomUrlRange( const TextWithEntities &text, const std::vector &links, int offset, int length) { for (const auto &entity : text.entities) { if (entity.type() != EntityType::CustomUrl || entity.offset() != offset || entity.length() != length) { continue; } const auto index = PreparedLinkIndexFromCustomUrlData(entity.data()); if (!index) { continue; } for (const auto &link : links) { if (link.index == index) { return &link; } } } return nullptr; } [[nodiscard]] std::vector CollectPreparedBlocksByKind( const std::vector &blocks, PreparedBlockKind kind) { auto result = std::vector(); ForEachPreparedBlock(blocks, [&](const PreparedBlock &block) { if (block.kind == kind) { result.push_back(&block); } }); return result; } struct InlineTextObjectMatch { EntityInText entity; InlineTextObjectEntity object; }; [[nodiscard]] std::vector CollectInlineTextObjectMatches( const TextWithEntities &text) { auto result = std::vector(); for (const auto &entity : text.entities) { if (entity.type() != EntityType::CustomEmoji || entity.length() != 1 || entity.offset() < 0 || entity.offset() >= text.text.size() || text.text[entity.offset()] != QChar::ObjectReplacementCharacter) { continue; } if (const auto parsed = ParseInlineTextObjectEntity(entity.data())) { result.push_back(InlineTextObjectMatch{ .entity = entity, .object = *parsed, }); } } return result; } [[nodiscard]] bool TextHasInlineFormulaEntity( const TextWithEntities &text, const QString ©Source, const QString &trimmedTex = QString()) { for (const auto &match : CollectInlineTextObjectMatches(text)) { if (match.object.kind != InlineTextObjectKind::Formula) { continue; } const auto formula = std::get_if( &match.object.data); if (!formula) { continue; } if ((formula->copySource == copySource) && (trimmedTex.isEmpty() || formula->trimmedTex == trimmedTex)) { return true; } } return false; } [[nodiscard]] bool HasEntityType( const EntitiesInText &entities, EntityType type) { for (const auto &entity : entities) { if (entity.type() == type) { return true; } } return false; } void Check(bool condition, const QString &message, bool *ok) { if (condition) { return; } if (ok) { *ok = false; } PrintError(FromLatin1("assertion failed: ") + message); } void CheckParseSuccess( const QByteArray &source, const QString &label, bool *ok) { const auto parsed = ParseMarkdownForIv(source, ParseOptions{ label }); Check( parsed.ok, label + FromLatin1(" parse failed: ") + parsed.error, ok); } void CheckParseFailure( const QByteArray &source, const QString &label, const QString &expectedError, bool *ok) { const auto parsed = ParseMarkdownForIv(source, ParseOptions{ label }); Check( !parsed.ok, label + FromLatin1(" should fail"), ok); if (!parsed.ok) { Check( parsed.error == expectedError, label + FromLatin1(" error should be ") + expectedError + FromLatin1(", got ") + parsed.error, ok); } } void CheckValidationEdges(bool *ok) { const auto &limits = ParseLimitsForIv(); auto utf8BomSource = QByteArray::fromHex("EFBBBF"); utf8BomSource.append("# Title\n"); auto validatedUtf8Bom = CheckValidationSuccess( utf8BomSource, FromLatin1("utf8 bom"), ok); if (validatedUtf8Bom.ok) { Check( validatedUtf8Bom.source.normalized == QByteArray("# Title\n"), FromLatin1("utf8 bom normalized bytes"), ok); const auto parsedValidated = ParseMarkdownForIv( std::move(validatedUtf8Bom.source)); Check( parsedValidated.ok, FromLatin1("utf8 bom validated parse failed: ") + parsedValidated.error, ok); } CheckParseSuccess( utf8BomSource, FromLatin1("utf8 bom"), ok); CheckValidationFailure( QByteArray::fromHex("FFFE2300"), FromLatin1("utf16 bom"), FromLatin1("source-unsupported-bom"), ok); CheckParseFailure( QByteArray::fromHex("FFFE2300"), FromLatin1("utf16 bom"), FromLatin1("source-unsupported-bom"), ok); CheckValidationFailure( QByteArray("a\0b", 3), FromLatin1("nul byte"), FromLatin1("source-binary"), ok); CheckParseFailure( QByteArray("a\0b", 3), FromLatin1("nul byte"), FromLatin1("source-binary"), ok); CheckValidationFailure( QByteArray::fromHex("C328"), FromLatin1("invalid utf8"), FromLatin1("source-invalid-utf8"), ok); CheckParseFailure( QByteArray::fromHex("C328"), FromLatin1("invalid utf8"), FromLatin1("source-invalid-utf8"), ok); const auto oversizedSource = QByteArray(limits.maxSourceBytes + 1, 'a'); CheckValidationFailure( oversizedSource, FromLatin1("source size"), FromLatin1("source-too-large"), ok); CheckParseFailure( oversizedSource, FromLatin1("source size"), FromLatin1("source-too-large"), ok); auto oversizedFormula = QByteArray(); oversizedFormula.reserve(limits.maxFormulaBytes + 2); oversizedFormula.append('$'); oversizedFormula.append(QByteArray(limits.maxFormulaBytes + 1, '+')); oversizedFormula.append('$'); CheckParseFailure( oversizedFormula, FromLatin1("formula size"), FromLatin1("formula-too-large"), ok); const auto generated = QByteArray( "Inline code `$code$`.\n" "```\n" "$block$\n" "```\n" "Escaped \\$ and price $5.99$.\n" "Real $x + y$ done.\n"); const auto parsed = ParseMarkdownForIv( generated, ParseOptions{ FromLatin1("generated-edge-checks.md") }); Check( parsed.ok, FromLatin1("generated exclusions parse failed: ") + parsed.error, ok); if (parsed.ok) { Check( static_cast(parsed.document.formulas.size()) == 1, FromLatin1("generated exclusions formula count"), ok); Check( CountFormulas(parsed.document, MathKind::Inline) == 1, FromLatin1("generated exclusions inline formula count"), ok); Check( CountFormulas(parsed.document, MathKind::Display) == 0, FromLatin1("generated exclusions display formula count"), ok); Check( HasFormula(parsed.document, MathKind::Inline, FromLatin1("x + y")), FromLatin1("generated exclusions real formula"), ok); } } void CheckInlineHtmlCoverage(bool dump, bool *ok) { const auto source = QByteArray( "H2O\n" "E = mc2\n" "Highlighted text using HTML mark\n" "
\n"); const auto label = FromLatin1("generated-inline-html.md"); const auto parsed = ParseMarkdownForIv(source, ParseOptions{ label }); Check( parsed.ok, label + FromLatin1(" parse failed: ") + parsed.error, ok); if (!parsed.ok) { return; } const auto validated = CheckValidationSuccess(source, label, ok); if (!validated.ok) { return; } const auto parsedValidated = ParseMarkdownForIv(std::move(validated.source)); Check( parsedValidated.ok, label + FromLatin1(" validated parse failed: ") + parsedValidated.error, ok); if (!parsedValidated.ok) { return; } CheckMatchingParseCounts( parsed.document, parsedValidated.document, label, ok); if (dump) { PrintLine(DumpForDebug(parsed.document)); } const auto &document = parsed.document.document; const auto subscriptParagraph = FindNodeByKindAndLineRange( document, NodeKind::Paragraph, 1, 1); Check( subscriptParagraph != nullptr, label + FromLatin1(" subscript paragraph range"), ok); if (subscriptParagraph) { Check( HasExactInlineHtmlTriplet( *subscriptParagraph, FromLatin1(""), FromLatin1("2"), FromLatin1("")), label + FromLatin1(" subscript HTML triplet"), ok); } const auto superscriptParagraph = FindNodeByKindAndLineRange( document, NodeKind::Paragraph, 2, 2); Check( superscriptParagraph != nullptr, label + FromLatin1(" superscript paragraph range"), ok); if (superscriptParagraph) { Check( HasExactInlineHtmlTriplet( *superscriptParagraph, FromLatin1(""), FromLatin1("2"), FromLatin1("")), label + FromLatin1(" superscript HTML triplet"), ok); } const auto markParagraph = FindNodeByKindAndLineRange( document, NodeKind::Paragraph, 3, 3); Check( markParagraph != nullptr, label + FromLatin1(" mark paragraph range"), ok); if (markParagraph) { Check( HasExactInlineHtmlTriplet( *markParagraph, FromLatin1(""), FromLatin1("Highlighted text using HTML mark"), FromLatin1("")), label + FromLatin1(" mark HTML triplet"), ok); } const auto htmlLineBreakParagraph = FindNodeByKindAndLineRange( document, NodeKind::Paragraph, 4, 4); Check( htmlLineBreakParagraph != nullptr, label + FromLatin1(" html line break paragraph range"), ok); if (htmlLineBreakParagraph) { const auto brInline = FindHtmlInlineByRaw( *htmlLineBreakParagraph, FromLatin1("
")); Check( brInline != nullptr, label + FromLatin1(" html line break raw node"), ok); if (brInline) { Check( brInline->text.isEmpty() && brInline->children.empty(), label + FromLatin1(" html line break lone HtmlInline"), ok); } } } void CheckInlineHtmlPrepareCoverage(bool *ok) { const auto source = QByteArray( "HTML line break test:
\n" "This should also be on a new line.\n"); const auto label = FromLatin1("generated-inline-html-prepare.md"); const auto parsed = ParseMarkdownForIv(source, ParseOptions{ label }); Check( parsed.ok, label + FromLatin1(" parse failed: ") + parsed.error, ok); if (!parsed.ok) { return; } const auto prepared = PrepareParsedDocumentForTest( parsed.document, label, std::make_shared()); Check( !prepared.failure.failed(), label + FromLatin1(" prepare failure: ") + PrepareFailureReason(prepared.failure), ok); if (prepared.failure.failed()) { return; } const auto paragraph = FindPreparedParagraphContaining( prepared, FromLatin1("HTML line break test:")); Check( paragraph != nullptr, label + FromLatin1(" prepared paragraph found"), ok); if (!paragraph) { return; } const auto expected = FromLatin1( "HTML line break test:\nThis should also be on a new line."); const auto &text = paragraph->text.text; Check( text.contains(expected), label + FromLatin1(" prepared text keeps real newline"), ok); Check( !text.contains(FromLatin1("
")), label + FromLatin1(" prepared text drops literal br"), ok); Check( !text.contains(FromLatin1("\n ")), label + FromLatin1(" prepared text has no post-break space"), ok); } void CheckFixtureSemanticCoverage( const PreparedDocument &document, const QString &path, bool *ok) { auto footnoteReferences = std::vector(); CollectNodesByKind( document.document, NodeKind::FootnoteReference, &footnoteReferences); Check( footnoteReferences.size() >= 2, FromLatin1("markdown-example.md footnote reference count"), ok); if (footnoteReferences.size() >= 2) { Check( footnoteReferences[0]->footnoteLabel == FromLatin1("1") && footnoteReferences[0]->footnoteOrdinal == 1, FromLatin1("markdown-example.md first footnote reference label"), ok); Check( footnoteReferences[1]->footnoteLabel == FromLatin1("long-note") && footnoteReferences[1]->footnoteOrdinal == 2, FromLatin1("markdown-example.md second footnote reference label"), ok); } auto footnoteDefinitions = std::vector(); CollectNodesByKind( document.document, NodeKind::FootnoteDefinition, &footnoteDefinitions); Check( footnoteDefinitions.size() >= 2, FromLatin1("markdown-example.md footnote definition count"), ok); if (footnoteDefinitions.size() >= 2) { Check( footnoteDefinitions[0]->anchorId == FromLatin1("fn-1") && footnoteDefinitions[1]->anchorId == FromLatin1("fn-2"), FromLatin1("markdown-example.md footnote anchors"), ok); } const auto headingsLink = FindLinkByTarget(document.document, FromLatin1("#headings")); Check( headingsLink != nullptr, FromLatin1("markdown-example.md toc fragment link"), ok); const auto relativeLink = FindLinkByTarget( document.document, FromLatin1("./docs/getting-started.md")); Check( relativeLink != nullptr, FromLatin1("markdown-example.md relative link parse"), ok); const auto headings = FindNodeByKindAndLineRange( document.document, NodeKind::Heading, 27, 27); Check( headings != nullptr && headings->anchorId == FromLatin1("headings"), FromLatin1("markdown-example.md headings anchor id"), ok); const auto definitionLists = FindNodeByKindAndLineRange( document.document, NodeKind::Heading, 266, 266); Check( definitionLists != nullptr && definitionLists->anchorId == FromLatin1("definition-lists-renderer-dependent"), FromLatin1("markdown-example.md punctuation heading anchor id"), ok); const auto details = FindNodeByKindAndLineRange( document.document, NodeKind::HtmlBlock, 261, 264); Check( details != nullptr, FromLatin1("markdown-example.md details block range"), ok); if (details) { Check( details->htmlBlockKind == HtmlBlockKind::Details && details->detailsSummary == FromLatin1("Click to expand details/summary block"), FromLatin1("markdown-example.md details classification"), ok); } const auto comment = FindHtmlBlockContaining( document.document, FromLatin1("markdown-renderer-test")); Check( comment != nullptr && comment->htmlBlockKind == HtmlBlockKind::Comment, FromLatin1("markdown-example.md comment classification"), ok); Check( WarningContains(document, FromLatin1("Unsupported HTML block")), FromLatin1("markdown-example.md unsupported html warning"), ok); const auto duplicateHeadings = ParseMarkdownForIv( QByteArray("## Same\n## Same\n"), ParseOptions{ FromLatin1("generated-duplicate-headings.md") }); Check( duplicateHeadings.ok, FromLatin1("generated duplicate headings parse failed"), ok); if (duplicateHeadings.ok) { auto duplicateNodes = std::vector(); CollectNodesByKind( duplicateHeadings.document.document, NodeKind::Heading, &duplicateNodes); Check( duplicateNodes.size() == 2 && duplicateNodes[0]->anchorId == FromLatin1("same") && duplicateNodes[1]->anchorId == FromLatin1("same-2"), FromLatin1("generated duplicate headings anchors"), ok); } } void CheckInlineTextObjectPrepareCoverage(bool *ok) { const auto label = FromLatin1("generated-inline-text-objects.md"); const auto parsed = ParseMarkdownForIv( QByteArray( "Paragraph $a^2 + b^2 = c^2$ text.\n\n" "#### Heading $ax^2 + bx + c = 0$\n\n" "| Formula | Value |\n" "| --- | --- |\n" "| $x^n$ | n |\n"), ParseOptions{ label }); Check( parsed.ok, label + FromLatin1(" parse failed: ") + parsed.error, ok); if (!parsed.ok) { return; } const auto prepared = PrepareParsedDocumentForTest( parsed.document, label, std::make_shared()); Check( !prepared.failure.failed(), label + FromLatin1(" prepare failure: ") + PrepareFailureReason(prepared.failure), ok); if (prepared.failure.failed()) { return; } const PreparedBlock *paragraph = nullptr; const PreparedBlock *heading = nullptr; const PreparedBlock *table = nullptr; ForEachPreparedBlock(prepared.blocks.blocks, [&](const PreparedBlock &block) { if (!paragraph && block.kind == PreparedBlockKind::Paragraph && block.text.text.contains(FromLatin1("Paragraph"))) { paragraph = █ } if (!heading && block.kind == PreparedBlockKind::Heading && block.text.text.contains(FromLatin1("Heading"))) { heading = █ } if (!table && block.kind == PreparedBlockKind::Table) { table = █ } }); Check( paragraph != nullptr, label + FromLatin1(" prepared paragraph block"), ok); if (paragraph) { Check( paragraph->text.text.count(QChar::ObjectReplacementCharacter) == 1, label + FromLatin1(" paragraph ORC count"), ok); Check( TextHasInlineFormulaEntity( paragraph->text, FromLatin1("$a^2 + b^2 = c^2$"), FromLatin1("a^2 + b^2 = c^2")), label + FromLatin1(" paragraph inline formula entity"), ok); } Check( heading != nullptr, label + FromLatin1(" prepared heading block"), ok); if (heading) { Check( heading->text.text.count(QChar::ObjectReplacementCharacter) == 1, label + FromLatin1(" heading ORC count"), ok); Check( TextHasInlineFormulaEntity( heading->text, FromLatin1("$ax^2 + bx + c = 0$"), FromLatin1("ax^2 + bx + c = 0")), label + FromLatin1(" heading inline formula entity"), ok); } Check( table != nullptr, label + FromLatin1(" prepared table block"), ok); if (table) { Check( table->tableRows.size() == 2, label + FromLatin1(" table row count"), ok); if (table->tableRows.size() == 2 && table->tableRows[1].cells.size() == 2) { const auto &formulaCell = table->tableRows[1].cells[0]; Check( formulaCell.text.text.count(QChar::ObjectReplacementCharacter) == 1, label + FromLatin1(" table cell ORC count"), ok); Check( TextHasInlineFormulaEntity( formulaCell.text, FromLatin1("$x^n$"), FromLatin1("x^n")), label + FromLatin1(" table cell inline formula entity"), ok); } } } void CheckNativeInstantViewPrepareCoverage(bool *ok) { const auto entityLabel = u"native-iv-inline-image-entity"_q; const auto serialized = SerializeInlineTextObjectEntity({ .kind = InlineTextObjectKind::IvImage, .data = InlineTextObjectIvImageData{ .documentId = 8801, .width = 24, .height = 18, .replacementText = u"[inline image]"_q, }, }); Check(!serialized.isEmpty(), entityLabel + u" serialize"_q, ok); const auto parsedEntity = ParseInlineTextObjectEntity(serialized); Check(parsedEntity.has_value(), entityLabel + u" parse"_q, ok); if (parsedEntity) { Check( parsedEntity->kind == InlineTextObjectKind::IvImage, entityLabel + u" parsed kind"_q, ok); if (const auto image = std::get_if( &parsedEntity->data)) { Check( image->documentId == 8801, entityLabel + u" parsed document id"_q, ok); Check( image->width == 24 && image->height == 18, entityLabel + u" parsed size"_q, ok); Check( image->replacementText == u"[inline image]"_q, entityLabel + u" parsed replacement text"_q, ok); } else { Check(false, entityLabel + u" parsed payload"_q, ok); } } const auto videoFixture = NativeIvVideoFixture(); const auto audioFixture = NativeIvAudioFixture(); const auto mapFixture = NativeIvMapFixture(); const auto channelFixture = NativeIvChannelFixture(); const auto collageFixture = NativeIvCollageFixture(); const auto slideshowFixture = NativeIvSlideshowFixture(); auto placeholderFixtures = std::vector(); const auto addPlaceholder = [&]( NativeIvPlaceholderKind kind, QString caption) { placeholderFixtures.push_back({ .kind = kind, .caption = caption, .expectedLabel = NativeIvPlaceholderLabel(kind), .block = NativeIvPlaceholderBlock(kind, caption), }); }; addPlaceholder(NativeIvPlaceholderKind::Embed, u"Embed caption"_q); addPlaceholder(NativeIvPlaceholderKind::EmbedPost, u"Embed post caption"_q); auto supportedBlocks = QVector(); supportedBlocks.push_back(NativeIvInlineImageParagraph( 8801, 24, 18, u"Lead "_q, u" tail."_q)); supportedBlocks.push_back(NativeIvPhotoBlock( 9001, u"Photo caption"_q, u"Photo credit"_q, u"https://example.com/photo"_q)); supportedBlocks.push_back(videoFixture.block); for (const auto &fixture : placeholderFixtures) { supportedBlocks.push_back(fixture.block); } supportedBlocks.push_back(collageFixture.block); supportedBlocks.push_back(slideshowFixture.block); supportedBlocks.push_back(audioFixture.block); supportedBlocks.push_back(mapFixture.block); supportedBlocks.push_back(channelFixture.block); supportedBlocks.push_back(NativeIvCoveredPhotoBlock( 9002, u"Cover caption"_q)); auto supportedPhotos = QVector(); supportedPhotos.push_back(NativeIvPhoto(9001, 640, 360)); supportedPhotos.push_back(NativeIvPhoto(9002, 320, 200)); for (const auto &photo : collageFixture.photos) { supportedPhotos.push_back(photo); } auto supportedDocuments = QVector(); for (const auto &document : videoFixture.documents) { supportedDocuments.push_back(document); } for (const auto &document : audioFixture.documents) { supportedDocuments.push_back(document); } for (const auto &document : collageFixture.documents) { supportedDocuments.push_back(document); } for (const auto &document : slideshowFixture.documents) { supportedDocuments.push_back(document); } auto supportedSource = NativeIvSource( std::move(supportedBlocks), std::move(supportedPhotos), std::move(supportedDocuments)); const auto supported = TryPrepareNativeInstantView({ .source = &supportedSource, }); Check( supported.supported(), u"native-iv supported source classification"_q, ok); Check( !supported.unsupported() && !supported.failed(), u"native-iv supported source nonterminal classification"_q, ok); Check( !supported.content.failure.failed(), u"native-iv supported source failure state"_q, ok); Check( supported.content.blocks.blocks.size() == 11, u"native-iv supported prepared block count"_q, ok); const PreparedBlock *inlineParagraph = nullptr; const PreparedBlock *photo = nullptr; const PreparedBlock *video = nullptr; const PreparedBlock *collage = nullptr; const PreparedBlock *slideshow = nullptr; const PreparedBlock *audio = nullptr; const PreparedBlock *map = nullptr; const PreparedBlock *channel = nullptr; const PreparedBlock *coveredPhoto = nullptr; auto preparedPlaceholders = std::vector(); ForEachPreparedBlock( supported.content.blocks.blocks, [&](const PreparedBlock &block) { if (!inlineParagraph && block.kind == PreparedBlockKind::Paragraph && block.text.text.contains(QChar::ObjectReplacementCharacter)) { inlineParagraph = █ } if (!photo && block.kind == PreparedBlockKind::Photo && block.photo.photoId == 9001) { photo = █ } if (!video && block.kind == PreparedBlockKind::Video && block.video.media.id == 7001) { video = █ } if (!collage && block.kind == PreparedBlockKind::GroupedMedia && block.text.text == collageFixture.caption) { collage = █ } if (!slideshow && block.kind == PreparedBlockKind::GroupedMedia && block.text.text == slideshowFixture.caption) { slideshow = █ } if (!audio && block.kind == PreparedBlockKind::Audio && block.audio.documentId == 7002) { audio = █ } if (!map && block.kind == PreparedBlockKind::Map) { map = █ } if (!channel && block.kind == PreparedBlockKind::Channel && block.channel.channelId == 7006) { channel = █ } if (!coveredPhoto && block.kind == PreparedBlockKind::Photo && block.photo.photoId == 9002) { coveredPhoto = █ } if (block.kind == PreparedBlockKind::Placeholder) { preparedPlaceholders.push_back(&block); } }); Check( inlineParagraph != nullptr, u"native-iv inline-image paragraph block"_q, ok); if (inlineParagraph) { const auto matches = CollectInlineTextObjectMatches(inlineParagraph->text); Check( matches.size() == 1, u"native-iv inline-image entity count"_q, ok); Check( inlineParagraph->text.text.count( QChar::ObjectReplacementCharacter) == 1, u"native-iv inline-image ORC count"_q, ok); if (matches.size() == 1) { Check( matches[0].object.kind == InlineTextObjectKind::IvImage, u"native-iv inline-image prepared kind"_q, ok); if (const auto image = std::get_if( &matches[0].object.data)) { Check( image->documentId == 8801, u"native-iv inline-image prepared document id"_q, ok); Check( image->width == 24 && image->height == 18, u"native-iv inline-image prepared size"_q, ok); Check( image->replacementText == u"[image]"_q, u"native-iv inline-image prepared replacement text"_q, ok); } else { Check(false, u"native-iv inline-image prepared payload"_q, ok); } } } Check(photo != nullptr, u"native-iv photo block"_q, ok); if (photo) { Check( photo->text.text == u"Photo caption\nPhoto credit"_q, u"native-iv photo caption text"_q, ok); Check(photo->photo.photoId == 9001, u"native-iv photo id"_q, ok); Check( photo->photo.width == 640 && photo->photo.height == 360, u"native-iv photo aspect metadata"_q, ok); Check( photo->photo.urlOverride == u"https://example.com/photo"_q, u"native-iv photo url override"_q, ok); Check(photo->photo.viewerOpen, u"native-iv photo viewer flag"_q, ok); } Check(video != nullptr, u"native-iv video block"_q, ok); if (video) { Check( video->text.text == videoFixture.caption, u"native-iv video caption text"_q, ok); Check( video->video.media.kind == PreparedMediaItemKind::Document, u"native-iv video media kind"_q, ok); Check(video->video.media.id == 7001, u"native-iv video id"_q, ok); Check( video->video.media.width == 1280 && video->video.media.height == 720, u"native-iv video dimensions"_q, ok); } Check(audio != nullptr, u"native-iv audio block"_q, ok); if (audio) { Check( audio->text.text == audioFixture.caption, u"native-iv audio caption text"_q, ok); Check(audio->audio.documentId == 7002, u"native-iv audio id"_q, ok); Check(audio->audio.title == u"Song Title"_q, u"native-iv audio title"_q, ok); Check( audio->audio.performer == u"Sample Artist"_q, u"native-iv audio performer"_q, ok); Check( audio->audio.fileName == u"track.mp3"_q, u"native-iv audio file name"_q, ok); Check(audio->audio.duration == 215, u"native-iv audio duration"_q, ok); } Check(map != nullptr, u"native-iv map block"_q, ok); if (map) { Check( map->text.text == mapFixture.caption, u"native-iv map caption text"_q, ok); Check( map->map.latitude == 51.5007 && map->map.longitude == -0.1246, u"native-iv map coordinates"_q, ok); Check(map->map.accessHash == 880088, u"native-iv map access hash"_q, ok); Check( map->map.width == 320 && map->map.height == 180, u"native-iv map dimensions"_q, ok); Check(map->map.zoom == 13, u"native-iv map zoom"_q, ok); Check(!map->map.url.isEmpty(), u"native-iv map url"_q, ok); } Check(channel != nullptr, u"native-iv channel block"_q, ok); if (channel) { Check(channel->text.text.isEmpty(), u"native-iv channel text empty"_q, ok); Check( channel->channel.channelId == 7006, u"native-iv channel id"_q, ok); Check( channel->channel.title == u"Native IV Channel"_q, u"native-iv channel title"_q, ok); Check( channel->channel.username == u"nativeiv"_q, u"native-iv channel username"_q, ok); } const auto unresolvedChannelLabel = u"native-iv-channel-unresolved"_q; const auto unresolvedChannelContext = SerializeTestNativeIvChannelContext( 7006, u"nativeiv"_q); const auto parsedChannelContext = ParseTestNativeIvChannelContext( unresolvedChannelContext); Check( parsedChannelContext.channelId == 7006, unresolvedChannelLabel + u" parsed id"_q, ok); Check( parsedChannelContext.username == u"nativeiv"_q, unresolvedChannelLabel + u" parsed username"_q, ok); Check( ResolveTestNativeIvChannelUsername( QString(), parsedChannelContext.username) == u"nativeiv"_q, unresolvedChannelLabel + u" unresolved local peer uses cached-page username"_q, ok); Check( ResolveTestNativeIvChannelUsername( u"localnativeiv"_q, parsedChannelContext.username) == u"localnativeiv"_q, unresolvedChannelLabel + u" loaded local username wins"_q, ok); Check(collage != nullptr, u"native-iv collage block"_q, ok); if (collage) { Check( collage->groupedMedia.items.size() == 2, u"native-iv collage item count"_q, ok); Check( collage->text.text == collageFixture.caption, u"native-iv collage caption text"_q, ok); if (collage->groupedMedia.items.size() == 2) { Check( collage->groupedMedia.items[0].media.kind == PreparedMediaItemKind::Photo, u"native-iv collage first item kind"_q, ok); Check( collage->groupedMedia.items[0].media.id == 9102, u"native-iv collage first item id"_q, ok); Check( collage->groupedMedia.items[1].media.kind == PreparedMediaItemKind::Document, u"native-iv collage second item kind"_q, ok); Check( collage->groupedMedia.items[1].media.id == 7003, u"native-iv collage second item id"_q, ok); } } Check(slideshow != nullptr, u"native-iv slideshow block"_q, ok); if (slideshow) { Check( slideshow->groupedMedia.items.size() == 2, u"native-iv slideshow item count"_q, ok); Check( slideshow->text.text == slideshowFixture.caption, u"native-iv slideshow caption text"_q, ok); if (slideshow->groupedMedia.items.size() == 2) { Check( slideshow->groupedMedia.items[0].media.kind == PreparedMediaItemKind::Document, u"native-iv slideshow first item kind"_q, ok); Check( slideshow->groupedMedia.items[0].media.id == 7004, u"native-iv slideshow first item id"_q, ok); Check( slideshow->groupedMedia.items[1].media.kind == PreparedMediaItemKind::Document, u"native-iv slideshow second item kind"_q, ok); Check( slideshow->groupedMedia.items[1].media.id == 7005, u"native-iv slideshow second item id"_q, ok); } } Check( coveredPhoto != nullptr, u"native-iv covered photo unwrapped"_q, ok); if (coveredPhoto) { Check( coveredPhoto->text.text == u"Cover caption"_q, u"native-iv covered photo caption"_q, ok); Check( coveredPhoto->photo.photoId == 9002, u"native-iv covered photo id"_q, ok); Check( coveredPhoto->photo.width == 320 && coveredPhoto->photo.height == 200, u"native-iv covered photo size"_q, ok); } Check( preparedPlaceholders.size() == placeholderFixtures.size(), u"native-iv placeholder prepared count"_q, ok); for (const auto &fixture : placeholderFixtures) { const auto it = std::find_if( preparedPlaceholders.begin(), preparedPlaceholders.end(), [&](const PreparedBlock *block) { return block && block->placeholder.label == fixture.expectedLabel && block->text.text == fixture.caption; }); Check( it != preparedPlaceholders.end(), fixture.expectedLabel + u" prepared placeholder"_q, ok); if (it != preparedPlaceholders.end()) { const auto *block = *it; Check( block->placeholder.copyText == (fixture.expectedLabel + u"\n"_q + fixture.caption), fixture.expectedLabel + u" placeholder copy text"_q, ok); } } auto unsupportedBlocks = QVector(); unsupportedBlocks.push_back(MTP_pageBlockCover(MTP_pageBlockUnsupported())); auto unsupportedSource = NativeIvSource(std::move(unsupportedBlocks)); const auto unsupported = TryPrepareNativeInstantView({ .source = &unsupportedSource, }); Check( unsupported.supported(), u"native-iv unsupported block placeholder classification"_q, ok); Check( !unsupported.unsupported() && !unsupported.failed(), u"native-iv unsupported block nonterminal classification"_q, ok); Check( unsupported.content.blocks.blocks.size() == 1, u"native-iv unsupported block placeholder count"_q, ok); if (unsupported.content.blocks.blocks.size() == 1) { const auto &block = unsupported.content.blocks.blocks.front(); Check( block.kind == PreparedBlockKind::Placeholder, u"native-iv unsupported block placeholder kind"_q, ok); Check( block.placeholder.label == u"Unsupported Content"_q, u"native-iv unsupported block placeholder label"_q, ok); } auto missingPhotoSource = NativeIvSource(QVector{ NativeIvPhotoBlock(9999, u"Missing photo"_q), }); const auto missingPhoto = TryPrepareNativeInstantView({ .source = &missingPhotoSource, }); Check( missingPhoto.supported(), u"native-iv missing-photo placeholder classification"_q, ok); Check( missingPhoto.content.blocks.blocks.size() == 1, u"native-iv missing-photo placeholder count"_q, ok); if (missingPhoto.content.blocks.blocks.size() == 1) { const auto &block = missingPhoto.content.blocks.blocks.front(); Check( block.kind == PreparedBlockKind::Placeholder, u"native-iv missing-photo placeholder kind"_q, ok); Check( block.placeholder.label == u"Photo Placeholder"_q, u"native-iv missing-photo placeholder label"_q, ok); Check( block.text.text == u"Missing photo"_q, u"native-iv missing-photo placeholder caption"_q, ok); } auto missingVideoSource = NativeIvSource(QVector{ videoFixture.block, }); const auto missingVideo = TryPrepareNativeInstantView({ .source = &missingVideoSource, }); Check( missingVideo.supported(), u"native-iv missing-video placeholder classification"_q, ok); if (missingVideo.content.blocks.blocks.size() == 1) { const auto &block = missingVideo.content.blocks.blocks.front(); Check( block.kind == PreparedBlockKind::Placeholder, u"native-iv missing-video placeholder kind"_q, ok); Check( block.placeholder.label == u"Video Placeholder"_q, u"native-iv missing-video placeholder label"_q, ok); Check( block.text.text == videoFixture.caption, u"native-iv missing-video placeholder caption"_q, ok); } auto nonVideoVideoSource = NativeIvSource( QVector{ videoFixture.block }, QVector(), QVector{ NativeIvImageDocument(7001, 1280, 720, u"video-poster.jpg"_q), }); const auto nonVideoVideo = TryPrepareNativeInstantView({ .source = &nonVideoVideoSource, }); Check( nonVideoVideo.supported(), u"native-iv non-video-video placeholder classification"_q, ok); if (nonVideoVideo.content.blocks.blocks.size() == 1) { const auto &block = nonVideoVideo.content.blocks.blocks.front(); Check( block.kind == PreparedBlockKind::Placeholder, u"native-iv non-video-video placeholder kind"_q, ok); Check( block.placeholder.label == u"Video Placeholder"_q, u"native-iv non-video-video placeholder label"_q, ok); Check( block.text.text == videoFixture.caption, u"native-iv non-video-video placeholder caption"_q, ok); } auto duplicatePoorerVideoSource = NativeIvSource( QVector{ videoFixture.block }, QVector(), videoFixture.documents); duplicatePoorerVideoSource.webpageDocument = NativeIvDocument( 7001, u"video/mp4"_q, QVector()); const auto duplicatePoorerVideo = TryPrepareNativeInstantView({ .source = &duplicatePoorerVideoSource, }); Check( duplicatePoorerVideo.supported(), u"native-iv duplicate poorer video classification"_q, ok); if (duplicatePoorerVideo.content.blocks.blocks.size() == 1) { const auto &block = duplicatePoorerVideo.content.blocks.blocks.front(); Check( block.kind == PreparedBlockKind::Video, u"native-iv duplicate poorer video kind"_q, ok); if (block.kind == PreparedBlockKind::Video) { Check( block.video.media.id == 7001, u"native-iv duplicate poorer video id"_q, ok); Check( block.video.media.width == 1280 && block.video.media.height == 720, u"native-iv duplicate poorer video dimensions"_q, ok); } } auto missingAudioSource = NativeIvSource(QVector{ audioFixture.block, }); const auto missingAudio = TryPrepareNativeInstantView({ .source = &missingAudioSource, }); Check( missingAudio.supported(), u"native-iv missing-audio placeholder classification"_q, ok); if (missingAudio.content.blocks.blocks.size() == 1) { const auto &block = missingAudio.content.blocks.blocks.front(); Check( block.kind == PreparedBlockKind::Placeholder, u"native-iv missing-audio placeholder kind"_q, ok); Check( block.placeholder.label == u"Audio File Placeholder"_q, u"native-iv missing-audio placeholder label"_q, ok); Check( block.text.text == audioFixture.caption, u"native-iv missing-audio placeholder caption"_q, ok); } auto invalidMapBlocks = QVector(); invalidMapBlocks.push_back(MTP_pageBlockMap( MTP_geoPointEmpty(), MTP_int(13), MTP_int(320), MTP_int(180), NativeIvCaption(u"Invalid map"_q))); auto invalidMapSource = NativeIvSource(std::move(invalidMapBlocks)); const auto invalidMap = TryPrepareNativeInstantView({ .source = &invalidMapSource, }); Check( invalidMap.supported(), u"native-iv invalid-map placeholder classification"_q, ok); if (invalidMap.content.blocks.blocks.size() == 1) { const auto &block = invalidMap.content.blocks.blocks.front(); Check( block.kind == PreparedBlockKind::Placeholder, u"native-iv invalid-map placeholder kind"_q, ok); Check( block.placeholder.label == u"Map Placeholder"_q, u"native-iv invalid-map placeholder label"_q, ok); Check( block.text.text == u"Invalid map"_q, u"native-iv invalid-map placeholder caption"_q, ok); } auto unsupportedCollageItems = QVector(); unsupportedCollageItems.push_back(NativeIvPhotoBlock(9102)); unsupportedCollageItems.push_back(MTP_pageBlockParagraph(NativeIvText( u"unsupported child"_q))); auto unsupportedCollageSource = NativeIvSource( QVector{ MTP_pageBlockCollage( MTP_vector(std::move(unsupportedCollageItems)), NativeIvCaption(u"Broken collage"_q)), }, collageFixture.photos, collageFixture.documents); const auto unsupportedCollage = TryPrepareNativeInstantView({ .source = &unsupportedCollageSource, }); Check( unsupportedCollage.supported(), u"native-iv unsupported-collage placeholder classification"_q, ok); if (unsupportedCollage.content.blocks.blocks.size() == 1) { const auto &block = unsupportedCollage.content.blocks.blocks.front(); Check( block.kind == PreparedBlockKind::Placeholder, u"native-iv unsupported-collage placeholder kind"_q, ok); Check( block.placeholder.label == u"Collage placeholder"_q, u"native-iv unsupported-collage placeholder label"_q, ok); Check( block.text.text == u"Broken collage"_q, u"native-iv unsupported-collage placeholder caption"_q, ok); } auto nonVideoCollageSource = NativeIvSource( QVector{ collageFixture.block }, collageFixture.photos, QVector{ NativeIvImageDocument(7003, 640, 360, u"collage-still.jpg"_q), }); const auto nonVideoCollage = TryPrepareNativeInstantView({ .source = &nonVideoCollageSource, }); Check( nonVideoCollage.supported(), u"native-iv non-video-collage placeholder classification"_q, ok); if (nonVideoCollage.content.blocks.blocks.size() == 1) { const auto &block = nonVideoCollage.content.blocks.blocks.front(); Check( block.kind == PreparedBlockKind::Placeholder, u"native-iv non-video-collage placeholder kind"_q, ok); Check( block.placeholder.label == u"Collage placeholder"_q, u"native-iv non-video-collage placeholder label"_q, ok); Check( block.text.text == collageFixture.caption, u"native-iv non-video-collage placeholder caption"_q, ok); } auto nonVideoSlideshowSource = NativeIvSource( QVector{ slideshowFixture.block }, slideshowFixture.photos, QVector{ NativeIvImageDocument(7004, 960, 540, u"slide-a.jpg"_q), slideshowFixture.documents[1], }); const auto nonVideoSlideshow = TryPrepareNativeInstantView({ .source = &nonVideoSlideshowSource, }); Check( nonVideoSlideshow.supported(), u"native-iv non-video-slideshow placeholder classification"_q, ok); if (nonVideoSlideshow.content.blocks.blocks.size() == 1) { const auto &block = nonVideoSlideshow.content.blocks.blocks.front(); Check( block.kind == PreparedBlockKind::Placeholder, u"native-iv non-video-slideshow placeholder kind"_q, ok); Check( block.placeholder.label == u"Grouped Media Placeholder"_q, u"native-iv non-video-slideshow placeholder label"_q, ok); Check( block.text.text == slideshowFixture.caption, u"native-iv non-video-slideshow placeholder caption"_q, ok); } auto missingGroupedDocumentSource = NativeIvSource( QVector{ slideshowFixture.block }, slideshowFixture.photos, QVector{ slideshowFixture.documents.front() }); const auto missingGroupedDocument = TryPrepareNativeInstantView({ .source = &missingGroupedDocumentSource, }); Check( missingGroupedDocument.supported(), u"native-iv missing-grouped-document placeholder classification"_q, ok); if (missingGroupedDocument.content.blocks.blocks.size() == 1) { const auto &block = missingGroupedDocument.content.blocks.blocks.front(); Check( block.kind == PreparedBlockKind::Placeholder, u"native-iv missing-grouped-document placeholder kind"_q, ok); Check( block.placeholder.label == u"Grouped Media Placeholder"_q, u"native-iv missing-grouped-document placeholder label"_q, ok); Check( block.text.text == slideshowFixture.caption, u"native-iv missing-grouped-document placeholder caption"_q, ok); } } void CheckCodeBlockTrailingNewlineTrim(bool *ok) { const auto markdownLabel = u"generated-code-block-trailing-newline"_q; const auto parsed = ParseMarkdownForIv(QByteArray(R"(```cpp extra-token alpha ``` beta )"), ParseOptions{ markdownLabel }); Check( parsed.ok, markdownLabel + u" parse failed: "_q + parsed.error, ok); if (!parsed.ok) { return; } auto renderer = std::make_shared(); const auto prepared = PrepareParsedDocumentForTest( parsed.document, markdownLabel, renderer); Check( !prepared.failure.failed(), markdownLabel + u" prepare failed: "_q + PrepareFailureReason(prepared.failure), ok); if (prepared.failure.failed()) { return; } const auto markdownCodeBlocks = CollectPreparedBlocksByKind( prepared.blocks.blocks, PreparedBlockKind::CodeBlock); Check( markdownCodeBlocks.size() == 2, markdownLabel + u" code block count"_q, ok); if (markdownCodeBlocks.size() == 2) { Check( markdownCodeBlocks[0]->text.text == u"alpha\n"_q, markdownLabel + u" fenced block trims one newline"_q, ok); Check( markdownCodeBlocks[0]->codeLanguage == u"cpp"_q, markdownLabel + u" fenced block keeps first info token"_q, ok); Check( markdownCodeBlocks[1]->text.text == u"beta"_q, markdownLabel + u" indented block trims one newline"_q, ok); Check( markdownCodeBlocks[1]->codeLanguage.isEmpty(), markdownLabel + u" indented block keeps empty language"_q, ok); } const auto nativeLabel = u"native-iv-preformatted-trailing-newline"_q; auto nativeBlocks = QVector(); nativeBlocks.push_back(MTP_pageBlockPreformatted( NativeIvText(u"single\n"_q), MTP_string(" txt "))); nativeBlocks.push_back(MTP_pageBlockPreformatted( NativeIvText(u"double\n\n"_q), MTP_string(" txt "))); auto nativeSource = NativeIvSource(std::move(nativeBlocks)); const auto nativePrepared = TryPrepareNativeInstantView({ .source = &nativeSource, }); Check( nativePrepared.supported(), nativeLabel + u" prepare supported"_q, ok); Check( !nativePrepared.content.failure.failed(), nativeLabel + u" prepare failed"_q, ok); if (!nativePrepared.supported() || nativePrepared.content.failure.failed()) { return; } const auto nativeCodeBlocks = CollectPreparedBlocksByKind( nativePrepared.content.blocks.blocks, PreparedBlockKind::CodeBlock); Check( nativeCodeBlocks.size() == 2, nativeLabel + u" code block count"_q, ok); if (nativeCodeBlocks.size() == 2) { Check( nativeCodeBlocks[0]->text.text == u"single"_q, nativeLabel + u" single trailing newline trimmed"_q, ok); Check( nativeCodeBlocks[0]->codeLanguage == u"txt"_q, nativeLabel + u" native language trimmed"_q, ok); Check( nativeCodeBlocks[1]->text.text == u"double\n"_q, nativeLabel + u" extra trailing newline preserved"_q, ok); Check( nativeCodeBlocks[1]->codeLanguage == u"txt"_q, nativeLabel + u" native language trimmed on second block"_q, ok); } } void CheckCodeBlockSelectionExportCoverage(bool *ok) { const auto renderer = std::make_shared(); const auto checkSelectionExport = [&](std::unique_ptr article, const QString &expectedLanguage, const QString &label) { Check( article != nullptr, label + u" article built"_q, ok); if (!article) { return; } Check( article->segmentIsText(0), label + u" code block segment is text"_q, ok); const auto length = article->segmentLength(0); Check( length >= 4, label + u" code block segment length"_q, ok); if (!article->segmentIsText(0) || (length < 4)) { return; } const auto checkExport = [&](const TextForMimeData &exported, const QString &selectionLabel) { auto preCount = 0; const auto pre = [&] { const EntityInText *result = nullptr; for (const auto &entity : exported.rich.entities) { if (entity.type() != EntityType::Pre) { continue; } ++preCount; if (!result) { result = &entity; } } return result; }(); Check( preCount == 1, selectionLabel + u" exports exactly one pre entity"_q, ok); Check( pre != nullptr && pre->offset() == 0 && pre->length() == exported.rich.text.size(), selectionLabel + u" pre entity spans exported text"_q, ok); Check( pre != nullptr && pre->data() == expectedLanguage, selectionLabel + u" pre entity keeps language"_q, ok); }; checkExport( article->textForSelection({ .from = { .segment = 0, .offset = 0 }, .to = { .segment = 0, .offset = length }, }, nullptr), label + u" full selection"_q); checkExport( article->textForSelection({ .from = { .segment = 0, .offset = 1 }, .to = { .segment = 0, .offset = length - 1 }, }, nullptr), label + u" partial selection"_q); }; const auto markdownLabel = u"generated-code-block-selection-language"_q; const auto markdownParsed = ParseMarkdownForIv(QByteArray(R"(```cpp ignored-token alpha beta ``` )"), ParseOptions{ markdownLabel }); Check( markdownParsed.ok, markdownLabel + u" parse failed: "_q + markdownParsed.error, ok); if (markdownParsed.ok) { auto markdownPrepared = PrepareParsedDocumentForTest( markdownParsed.document, markdownLabel, renderer); Check( !markdownPrepared.failure.failed(), markdownLabel + u" prepare failed: "_q + PrepareFailureReason(markdownPrepared.failure), ok); if (!markdownPrepared.failure.failed()) { auto markdownArticleHeight = 0; checkSelectionExport( BuildArticleForTest( std::move(markdownPrepared), renderer, 420, &markdownArticleHeight), u"cpp"_q, markdownLabel); } } const auto nativeLabel = u"native-iv-code-block-selection-language"_q; auto nativeBlocks = QVector(); nativeBlocks.push_back(MTP_pageBlockPreformatted( NativeIvText(u"alpha\nbeta\n"_q), MTP_string(" rust "))); auto nativeSource = NativeIvSource(std::move(nativeBlocks)); auto nativePrepared = TryPrepareNativeInstantView({ .source = &nativeSource, }); Check( nativePrepared.supported(), nativeLabel + u" prepare supported"_q, ok); Check( !nativePrepared.content.failure.failed(), nativeLabel + u" prepare failed"_q, ok); if (nativePrepared.supported() && !nativePrepared.content.failure.failed()) { auto nativeArticleHeight = 0; checkSelectionExport( BuildArticleForTest( std::move(nativePrepared.content), renderer, 420, &nativeArticleHeight), u"rust"_q, nativeLabel); } } void CheckCodeBlockAsyncSyntaxHighlightCoverage(bool *ok) { const auto label = u"generated-code-block-async-highlight"_q; const auto parsed = ParseMarkdownForIv(QByteArray(R"(```cpp namespace phase3_async_highlight_unique_z { auto value = 42; auto text = "native-markdown-iv-phase-z-highlight"; } ``` ```cpp namespace phase3_async_highlight_unique_z { auto value = 42; auto text = "native-markdown-iv-phase-z-highlight"; } ``` )"), ParseOptions{ label }); Check( parsed.ok, label + u" parse failed: "_q + parsed.error, ok); if (!parsed.ok) { return; } auto renderer = std::make_shared(); auto prepared = PrepareParsedDocumentForTest( parsed.document, label, renderer); Check( !prepared.failure.failed(), label + u" prepare failed: "_q + PrepareFailureReason(prepared.failure), ok); if (prepared.failure.failed()) { return; } auto height = 0; auto article = BuildArticleForTest( std::move(prepared), renderer, 420, &height); auto highlightWaiter = MarkdownArticleHighlightWaiter(article.get()); const auto firstImage = PaintArticleForSyntaxHighlightTest( article.get(), 420, height); Check( HasPaintedPixels(firstImage), label + u" first paint produced pixels"_q, ok); const auto firstBounds = SegmentHitBounds(article.get(), 420, height, 0); const auto repeatedBounds = SegmentHitBounds(article.get(), 420, height, 1); Check( firstBounds.has_value(), label + u" code block segment hit bounds"_q, ok); Check( repeatedBounds.has_value(), label + u" repeated code block segment hit bounds"_q, ok); const auto highlightCompleted = highlightWaiter.wait(); Check( highlightCompleted, label + u" async highlight completion"_q, ok); if (!firstBounds || !repeatedBounds || !highlightCompleted) { return; } const auto heightAfterHighlight = article->resizeGetHeight(420); const auto secondBounds = SegmentHitBounds( article.get(), 420, heightAfterHighlight, 0); const auto repeatedSecondBounds = SegmentHitBounds( article.get(), 420, heightAfterHighlight, 1); const auto secondImage = PaintArticleForSyntaxHighlightTest( article.get(), 420, heightAfterHighlight); Check( HasPaintedPixels(secondImage), label + u" second paint produced pixels"_q, ok); Check( heightAfterHighlight == height, label + u" highlight keeps article height"_q, ok); Check( secondBounds.has_value() && (*secondBounds == *firstBounds), label + u" highlight keeps code block bounds"_q, ok); Check( repeatedSecondBounds.has_value() && (*repeatedSecondBounds == *repeatedBounds), label + u" highlight keeps repeated code block bounds"_q, ok); Check( PixelsDifferInRect(firstImage, secondImage, *firstBounds), label + u" highlight repaint changes code block pixels"_q, ok); Check( PixelsDifferInRect(firstImage, secondImage, *repeatedBounds), label + u" highlight repaint changes repeated code block pixels"_q, ok); } void CheckNativeInstantViewArticleCoverage(bool *ok) { const auto renderer = std::make_shared(); auto lookupFlags = Ui::Text::StateRequest::Flags(); lookupFlags |= Ui::Text::StateRequest::Flag::LookupSymbol; const auto videoFixture = NativeIvVideoFixture(); const auto audioFixture = NativeIvAudioFixture(); const auto mapFixture = NativeIvMapFixture(); const auto channelFixture = NativeIvChannelFixture(); const auto collageFixture = NativeIvCollageFixture(); const auto slideshowFixture = NativeIvSlideshowFixture(); const auto mixedLabel = u"native-iv-mixed-article"_q; auto mixedRuntime = std::make_shared(); auto inlineImage = std::make_shared(); auto videoRuntime = std::make_shared(); videoRuntime->thumbnailImage = std::make_shared(); videoRuntime->fullImage = std::make_shared(); videoRuntime->loadingValue = true; videoRuntime->progressValue = 0.25; auto mixedPhotoRuntime = std::make_shared(); mixedPhotoRuntime->thumbnailImage = std::make_shared(); mixedPhotoRuntime->fullImage = std::make_shared(); mixedPhotoRuntime->loadingValue = true; mixedPhotoRuntime->progressValue = 0.5; mixedRuntime->addInlineImage(8801, inlineImage); mixedRuntime->addDocumentRuntime(7001, videoRuntime); mixedRuntime->addPhotoRuntime(9101, mixedPhotoRuntime); auto mixedBlocks = QVector(); mixedBlocks.push_back(NativeIvInlineImageParagraph( 8801, 24, 18, u"Intro "_q, u" tail."_q)); mixedBlocks.push_back(videoFixture.block); mixedBlocks.push_back(NativeIvPhotoBlock(9101)); mixedBlocks.push_back(MTP_pageBlockParagraph(NativeIvText( u"Outro paragraph."_q))); auto mixedPhotos = QVector(); mixedPhotos.push_back(NativeIvPhoto(9101, 320, 180)); auto mixedSource = NativeIvSource( std::move(mixedBlocks), std::move(mixedPhotos), videoFixture.documents); auto mixedPrepared = TryPrepareNativeInstantView({ .source = &mixedSource, .mediaRuntime = mixedRuntime, }); Check( mixedPrepared.supported(), mixedLabel + u" prepare supported"_q, ok); if (!mixedPrepared.supported()) { return; } Check( mixedPrepared.content.mediaRuntime.get() == mixedRuntime.get(), mixedLabel + u" media runtime forwarded"_q, ok); auto wideHeight = 0; auto mixedArticle = BuildArticleForTest( std::move(mixedPrepared.content), renderer, 520, &wideHeight); const auto wideImage = PaintArticleForTest( mixedArticle.get(), 520, wideHeight); Check( HasPaintedPixels(wideImage), mixedLabel + u" wide paint produced pixels"_q, ok); Check( !mixedRuntime->inlineRequests.empty(), mixedLabel + u" inline image resolve request"_q, ok); if (!mixedRuntime->inlineRequests.empty()) { Check( mixedRuntime->inlineRequests.front().first == 8801, mixedLabel + u" inline image resolved document id"_q, ok); Check( mixedRuntime->inlineRequests.front().second == QSize(24, 18), mixedLabel + u" inline image resolved size"_q, ok); } Check( !mixedRuntime->documentRequests.empty() && (mixedRuntime->documentRequests.front() == 7001), mixedLabel + u" video runtime resolve request"_q, ok); Check( !mixedRuntime->photoRequests.empty() && (mixedRuntime->photoRequests.front() == 9101), mixedLabel + u" photo runtime resolve request"_q, ok); Check( !videoRuntime->thumbnailSizes.empty() && !videoRuntime->fullSizes.empty(), mixedLabel + u" video image size requests"_q, ok); Check( !mixedPhotoRuntime->thumbnailSizes.empty() && !mixedPhotoRuntime->fullSizes.empty(), mixedLabel + u" photo image size requests"_q, ok); Check( mixedArticle->segmentIsText(0), mixedLabel + u" inline-image paragraph remains text segment"_q, ok); if (mixedArticle->segmentIsText(0)) { const auto exported = mixedArticle->textForSelection({ .from = { .segment = 0, .offset = 0 }, .to = { .segment = 0, .offset = mixedArticle->segmentLength(0), }, }, nullptr); Check( exported.expanded == u"Intro [image] tail."_q, mixedLabel + u" inline-image export text"_q, ok); Check( !HasEntityType(exported.rich.entities, EntityType::CustomEmoji), mixedLabel + u" inline-image export drops custom entity"_q, ok); } const auto narrowHeight = mixedArticle->resizeGetHeight(260); const auto narrowImage = PaintArticleForTest( mixedArticle.get(), 260, narrowHeight); Check( HasPaintedPixels(narrowImage), mixedLabel + u" narrow paint produced pixels"_q, ok); Check( narrowHeight > wideHeight, mixedLabel + u" narrow relayout grows height"_q, ok); Check( inlineImage->subscriptionCount >= 1, mixedLabel + u" inline image subscribed for repaint"_q, ok); Check( !inlineImage->requestedSizes.empty() && (inlineImage->requestedSizes.front() == 24), mixedLabel + u" inline image paint request size"_q, ok); auto segmentBounds = std::vector>(); for (auto segmentIndex = 0; segmentIndex != 4; ++segmentIndex) { segmentBounds.push_back(SegmentHitBounds( mixedArticle.get(), 260, narrowHeight, segmentIndex)); Check( segmentBounds.back().has_value(), mixedLabel + u" segment hit bounds "_q + QString::number(segmentIndex), ok); } auto haveBounds = true; for (const auto &bounds : segmentBounds) { if (!bounds) { haveBounds = false; break; } } if (!haveBounds) { return; } for (auto segmentIndex = 1; segmentIndex != 4; ++segmentIndex) { Check( segmentBounds[segmentIndex]->top() > segmentBounds[segmentIndex - 1]->bottom(), mixedLabel + u" segment order "_q + QString::number(segmentIndex), ok); } const auto videoHit = mixedArticle->hitTest( segmentBounds[1]->center(), lookupFlags); Check( videoHit.valid() && videoHit.direct && (videoHit.segmentIndex == 1), mixedLabel + u" video direct hit"_q, ok); Check( videoHit.mediaActivation.kind == MediaActivationKind::Document, mixedLabel + u" video activation kind"_q, ok); Check( videoHit.mediaActivation.document.get() == videoRuntime.get(), mixedLabel + u" video runtime forwarded to hit"_q, ok); Check( !videoHit.preparedLink.has_value(), mixedLabel + u" video hit has no prepared link"_q, ok); const auto videoContext = mixedArticle->textForContext(videoHit); Check( videoContext.expanded == (tr::lng_in_dlg_video(tr::now) + u"\n"_q + videoFixture.caption), mixedLabel + u" video context export text"_q, ok); const auto videoSelection = mixedArticle->textForSelection({ .from = { .segment = 1, .offset = 0 }, .to = { .segment = 1, .offset = 1 }, }, nullptr); Check( videoSelection.expanded == videoContext.expanded, mixedLabel + u" video selection export text"_q, ok); if (videoHit.mediaActivation.document) { videoHit.mediaActivation.document->open(Qt::LeftButton); } Check( videoRuntime->openedButtons.size() == 1 && (videoRuntime->openedButtons.front() == Qt::LeftButton), mixedLabel + u" video open intent"_q, ok); const auto photoHit = mixedArticle->hitTest( segmentBounds[2]->center(), lookupFlags); Check( photoHit.valid() && photoHit.direct && (photoHit.segmentIndex == 2), mixedLabel + u" photo direct hit"_q, ok); Check( photoHit.mediaActivation.kind == MediaActivationKind::Photo, mixedLabel + u" photo activation kind"_q, ok); Check( photoHit.mediaActivation.photo.get() == mixedPhotoRuntime.get(), mixedLabel + u" photo runtime forwarded to hit"_q, ok); if (photoHit.mediaActivation.photo) { photoHit.mediaActivation.photo->open(Qt::LeftButton); } Check( mixedPhotoRuntime->openedButtons.size() == 1 && (mixedPhotoRuntime->openedButtons.front() == Qt::LeftButton), mixedLabel + u" photo open intent"_q, ok); inlineImage->setFrame(SolidTestImage(24, 18, QColor(0, 160, 220))); inlineImage->notify(); videoRuntime->thumbnailImage->setFrame(SolidTestImage( 160, 90, QColor(20, 120, 220))); videoRuntime->fullImage->setFrame(SolidTestImage( 320, 180, QColor(0, 120, 90))); videoRuntime->loadingValue = false; videoRuntime->loadedValue = true; videoRuntime->progressValue = 1.; mixedPhotoRuntime->thumbnailImage->setFrame(SolidTestImage( 160, 90, QColor(220, 160, 0))); mixedPhotoRuntime->fullImage->setFrame(SolidTestImage( 320, 180, QColor(0, 200, 90))); mixedPhotoRuntime->loadingValue = false; mixedPhotoRuntime->loadedValue = true; mixedPhotoRuntime->progressValue = 1.; const auto narrowHeightAfterUpdate = mixedArticle->resizeGetHeight(260); Check( narrowHeightAfterUpdate == narrowHeight, mixedLabel + u" media repaint keeps layout height"_q, ok); const auto updatedImage = PaintArticleForTest( mixedArticle.get(), 260, narrowHeightAfterUpdate); Check( HasPaintedPixels(updatedImage), mixedLabel + u" updated paint produced pixels"_q, ok); const auto updatedVideoBounds = SegmentHitBounds( mixedArticle.get(), 260, narrowHeightAfterUpdate, 1); const auto updatedPhotoBounds = SegmentHitBounds( mixedArticle.get(), 260, narrowHeightAfterUpdate, 2); const auto updatedInlineBounds = SegmentHitBounds( mixedArticle.get(), 260, narrowHeightAfterUpdate, 0); Check( updatedVideoBounds.has_value() && (*updatedVideoBounds == *segmentBounds[1]), mixedLabel + u" video repaint keeps bounds"_q, ok); Check( updatedPhotoBounds.has_value() && (*updatedPhotoBounds == *segmentBounds[2]), mixedLabel + u" photo repaint keeps bounds"_q, ok); Check( updatedInlineBounds.has_value() && (*updatedInlineBounds == *segmentBounds[0]), mixedLabel + u" inline-image repaint keeps bounds"_q, ok); const auto audioLabel = u"native-iv-audio-article"_q; auto audioRuntime = std::make_shared(); auto audioDocumentRuntime = std::make_shared(); audioRuntime->addDocumentRuntime(7002, audioDocumentRuntime); auto audioSource = NativeIvSource( QVector{ audioFixture.block }, audioFixture.photos, audioFixture.documents); auto audioPrepared = TryPrepareNativeInstantView({ .source = &audioSource, .mediaRuntime = audioRuntime, }); Check(audioPrepared.supported(), audioLabel + u" prepare supported"_q, ok); if (audioPrepared.supported()) { auto audioHeight = 0; auto audioArticle = BuildArticleForTest( std::move(audioPrepared.content), renderer, 420, &audioHeight); const auto audioImage = PaintArticleForTest( audioArticle.get(), 420, audioHeight); Check( HasPaintedPixels(audioImage), audioLabel + u" paint produced pixels"_q, ok); Check( audioRuntime->documentRequests.size() == 1 && (audioRuntime->documentRequests.front() == 7002), audioLabel + u" document resolve request"_q, ok); const auto audioBounds = SegmentHitBounds( audioArticle.get(), 420, audioHeight, 0); Check(audioBounds.has_value(), audioLabel + u" media bounds"_q, ok); if (audioBounds) { const auto audioHit = audioArticle->hitTest( audioBounds->center(), lookupFlags); Check( audioHit.valid() && audioHit.direct && (audioHit.segmentIndex == 0), audioLabel + u" direct hit"_q, ok); Check( audioHit.mediaActivation.kind == MediaActivationKind::Document, audioLabel + u" activation kind"_q, ok); Check( audioHit.mediaActivation.document.get() == audioDocumentRuntime.get(), audioLabel + u" runtime forwarded"_q, ok); const auto audioContext = audioArticle->textForContext(audioHit); Check( audioContext.expanded == u"Song Title\nSample Artist\nAudio caption"_q, audioLabel + u" context export text"_q, ok); const auto audioSelection = audioArticle->textForSelection({ .from = { .segment = 0, .offset = 0 }, .to = { .segment = 0, .offset = 1 }, }, nullptr); Check( audioSelection.expanded == audioContext.expanded, audioLabel + u" selection export text"_q, ok); if (audioHit.mediaActivation.document) { audioHit.mediaActivation.document->open(Qt::LeftButton); } Check( audioDocumentRuntime->openedButtons.size() == 1 && (audioDocumentRuntime->openedButtons.front() == Qt::LeftButton), audioLabel + u" open intent"_q, ok); } const auto audioCaptionSelection = audioArticle->textForSelection({ .from = { .segment = 1, .offset = 0 }, .to = { .segment = 1, .offset = audioArticle->segmentLength(1), }, }, nullptr); Check( audioCaptionSelection.expanded == audioFixture.caption, audioLabel + u" caption selection export"_q, ok); } const auto mapLabel = u"native-iv-map-article"_q; auto mapRuntime = std::make_shared(); auto mapImageRuntime = std::make_shared(); mapImageRuntime->thumbnailImage = std::make_shared(); mapImageRuntime->fullImage = std::make_shared(); mapRuntime->addMapRuntime(51.5007, -0.1246, 880088, 13, mapImageRuntime); auto mapSource = NativeIvSource(QVector{ mapFixture.block, }); auto mapPrepared = TryPrepareNativeInstantView({ .source = &mapSource, .mediaRuntime = mapRuntime, }); Check(mapPrepared.supported(), mapLabel + u" prepare supported"_q, ok); if (mapPrepared.supported()) { auto mapHeight = 0; auto mapArticle = BuildArticleForTest( std::move(mapPrepared.content), renderer, 420, &mapHeight); const auto mapImage = PaintArticleForTest( mapArticle.get(), 420, mapHeight); Check( HasPaintedPixels(mapImage), mapLabel + u" paint produced pixels"_q, ok); Check( mapRuntime->mapRequests.size() == 1, mapLabel + u" runtime resolve request"_q, ok); if (!mapRuntime->mapRequests.empty()) { const auto &request = mapRuntime->mapRequests.front(); Check( request.latitude == 51.5007 && request.longitude == -0.1246, mapLabel + u" coordinates forwarded"_q, ok); Check( request.accessHash == 880088 && request.zoom == 13, mapLabel + u" access hash and zoom forwarded"_q, ok); } const auto mapBounds = SegmentHitBounds( mapArticle.get(), 420, mapHeight, 0); Check(mapBounds.has_value(), mapLabel + u" media bounds"_q, ok); if (mapBounds) { const auto mapHit = mapArticle->hitTest( mapBounds->center(), lookupFlags); Check( mapHit.valid() && mapHit.direct && (mapHit.segmentIndex == 0), mapLabel + u" direct hit"_q, ok); Check( mapHit.mediaActivation.kind == MediaActivationKind::ExternalUrl, mapLabel + u" activation kind"_q, ok); Check( mapHit.preparedLink.has_value() && !mapHit.preparedLink->target.isEmpty(), mapLabel + u" prepared link created"_q, ok); if (mapHit.preparedLink) { Check( mapHit.mediaActivation.url == mapHit.preparedLink->target, mapLabel + u" media activation url matches prepared link"_q, ok); } const auto mapContext = mapArticle->textForContext(mapHit); Check( mapContext.expanded == (tr::lng_maps_point(tr::now) + u"\n"_q + mapFixture.caption), mapLabel + u" context export text"_q, ok); const auto mapSelection = mapArticle->textForSelection({ .from = { .segment = 0, .offset = 0 }, .to = { .segment = 0, .offset = 1 }, }, nullptr); Check( mapSelection.expanded == mapContext.expanded, mapLabel + u" selection export text"_q, ok); } const auto mapCaptionSelection = mapArticle->textForSelection({ .from = { .segment = 1, .offset = 0 }, .to = { .segment = 1, .offset = mapArticle->segmentLength(1), }, }, nullptr); Check( mapCaptionSelection.expanded == mapFixture.caption, mapLabel + u" caption selection export"_q, ok); } const auto channelLabel = u"native-iv-channel-article"_q; auto channelRuntime = std::make_shared(); auto channelCardRuntime = std::make_shared(); channelRuntime->addChannelRuntime(7006, u"nativeiv"_q, channelCardRuntime); auto channelSource = NativeIvSource(QVector{ channelFixture.block, }); auto channelPrepared = TryPrepareNativeInstantView({ .source = &channelSource, .mediaRuntime = channelRuntime, }); Check( channelPrepared.supported(), channelLabel + u" prepare supported"_q, ok); if (channelPrepared.supported()) { auto channelHeight = 0; auto channelArticle = BuildArticleForTest( std::move(channelPrepared.content), renderer, 420, &channelHeight); const auto channelBefore = PaintArticleForTest( channelArticle.get(), 420, channelHeight); Check( HasPaintedPixels(channelBefore), channelLabel + u" paint produced pixels"_q, ok); Check( channelRuntime->channelRequests.size() == 1 && (channelRuntime->channelRequests.front().channelId == 7006) && (channelRuntime->channelRequests.front().username == u"nativeiv"_q), channelLabel + u" runtime resolve request"_q, ok); const auto openBounds = HitBoundsWhere( channelArticle.get(), 420, channelHeight, lookupFlags, [](const MarkdownArticleHitTestResult &hit) { return hit.mediaActivation.kind == MediaActivationKind::OpenChannel; }); const auto joinBounds = HitBoundsWhere( channelArticle.get(), 420, channelHeight, lookupFlags, [](const MarkdownArticleHitTestResult &hit) { return hit.mediaActivation.kind == MediaActivationKind::JoinChannel; }); Check(openBounds.has_value(), channelLabel + u" open bounds"_q, ok); Check(joinBounds.has_value(), channelLabel + u" join bounds"_q, ok); if (openBounds) { const auto openHit = channelArticle->hitTest( openBounds->center(), lookupFlags); Check( openHit.mediaActivation.kind == MediaActivationKind::OpenChannel, channelLabel + u" open activation kind"_q, ok); Check( openHit.mediaActivation.channel.get() == channelCardRuntime.get(), channelLabel + u" open runtime forwarded"_q, ok); if (openHit.mediaActivation.channel) { openHit.mediaActivation.channel->open(Qt::LeftButton); } } if (joinBounds) { const auto joinHit = channelArticle->hitTest( joinBounds->center(), lookupFlags); Check( joinHit.mediaActivation.kind == MediaActivationKind::JoinChannel, channelLabel + u" join activation kind"_q, ok); Check( joinHit.mediaActivation.channel.get() == channelCardRuntime.get(), channelLabel + u" join runtime forwarded"_q, ok); const auto joinContext = channelArticle->textForContext(joinHit); Check( joinContext.expanded == u"Native IV Channel\n@nativeiv"_q, channelLabel + u" context export text"_q, ok); const auto joinSelection = channelArticle->textForSelection({ .from = { .segment = 0, .offset = 0 }, .to = { .segment = 0, .offset = 1 }, }, nullptr); Check( joinSelection.expanded == joinContext.expanded, channelLabel + u" selection export text"_q, ok); if (joinHit.mediaActivation.channel) { joinHit.mediaActivation.channel->join(Qt::LeftButton); } } Check( channelCardRuntime->openedButtons.size() == 1 && (channelCardRuntime->openedButtons.front() == Qt::LeftButton), channelLabel + u" open intent"_q, ok); Check( channelCardRuntime->joinedButtons.size() == 1 && (channelCardRuntime->joinedButtons.front() == Qt::LeftButton), channelLabel + u" join intent"_q, ok); if (joinBounds) { channelCardRuntime->joinVisibleValue = false; const auto joinedHit = channelArticle->hitTest( joinBounds->center(), lookupFlags); Check( joinedHit.mediaActivation.kind == MediaActivationKind::OpenChannel, channelLabel + u" joined state hides join action"_q, ok); const auto channelAfter = PaintArticleForTest( channelArticle.get(), 420, channelHeight); Check( PixelsDifferInRect(channelBefore, channelAfter, *joinBounds), channelLabel + u" joined state repaint changes button pixels"_q, ok); } auto previewRuntime = std::make_shared(); auto previewChannelRuntime = std::make_shared(); previewRuntime->addChannelRuntime( 7006, u"nativeiv"_q, previewChannelRuntime); auto previewSource = NativeIvSource(QVector{ channelFixture.block, }); auto previewPrepared = TryPrepareNativeInstantView({ .source = &previewSource, .mediaRuntime = previewRuntime, }); Check( previewPrepared.supported(), channelLabel + u" preview prepare supported"_q, ok); if (previewPrepared.supported()) { auto window = Ui::RpWindow(); window.setGeometry(QRect(0, 0, 420, 240)); window.show(); FlushPendingWidgetEvents(); auto preview = CreateMarkdownPreviewWidget( window.body(), std::move(previewPrepared.content), renderer, [](Event) { }); preview->setGeometry(QRect(QPoint(), window.body()->size())); preview->show(); FlushPendingWidgetEvents(); const auto body = FindChildObject( preview.get()); Check( body != nullptr, channelLabel + u" preview body widget"_q, ok); if (body) { auto counter = WidgetEventCounter(); body->installEventFilter(&counter); const auto previewBefore = RenderWidgetForTest(body); counter.reset(); previewChannelRuntime->joinVisibleValue = false; previewRuntime->fireChannelJoinedChange(7006); FlushPendingWidgetEvents(); const auto repainted = (counter.paintCount > 0) || (counter.updateRequestCount > 0); const auto previewAfter = RenderWidgetForTest(body); Check( repainted, channelLabel + u" joined change triggers preview repaint"_q, ok); Check( PixelsDifferInRect( previewBefore, previewAfter, QRect(QPoint(), previewAfter.size())), channelLabel + u" preview repaint changes rendered pixels"_q, ok); body->removeEventFilter(&counter); } } } const auto channelRelayoutLabel = u"native-iv-channel-relayout"_q; const auto relayoutChannelId = uint64(7007); const auto relayoutChannelUsername = u"nativeinstantviewrelayout"_q; const auto relayoutChannelBlock = MTP_pageBlockChannel(NativeIvChannelChat( relayoutChannelId, u"Native Instant View Channel Title That Wraps When The Join Button Is Visible"_q, relayoutChannelUsername)); auto joinedLayoutRuntime = std::make_shared(); auto joinedLayoutChannelRuntime = std::make_shared(); joinedLayoutChannelRuntime->joinVisibleValue = false; joinedLayoutRuntime->addChannelRuntime( relayoutChannelId, relayoutChannelUsername, joinedLayoutChannelRuntime); auto joinedLayoutSource = NativeIvSource(QVector{ relayoutChannelBlock, }); auto joinedLayoutPrepared = TryPrepareNativeInstantView({ .source = &joinedLayoutSource, .mediaRuntime = joinedLayoutRuntime, }); Check( joinedLayoutPrepared.supported(), channelRelayoutLabel + u" hidden prepare supported"_q, ok); auto joinedLayoutHeight = 0; if (joinedLayoutPrepared.supported()) { auto joinedLayoutArticle = BuildArticleForTest( std::move(joinedLayoutPrepared.content), renderer, 320, &joinedLayoutHeight); const auto joinedLayoutJoinBounds = HitBoundsWhere( joinedLayoutArticle.get(), 320, joinedLayoutHeight, lookupFlags, [](const MarkdownArticleHitTestResult &hit) { return hit.mediaActivation.kind == MediaActivationKind::JoinChannel; }); Check( !joinedLayoutJoinBounds.has_value(), channelRelayoutLabel + u" hidden layout has no join bounds"_q, ok); } auto articleRelayoutRuntime = std::make_shared(); auto articleRelayoutChannelRuntime = std::make_shared(); articleRelayoutRuntime->addChannelRuntime( relayoutChannelId, relayoutChannelUsername, articleRelayoutChannelRuntime); auto articleRelayoutSource = NativeIvSource(QVector{ relayoutChannelBlock, }); auto articleRelayoutPrepared = TryPrepareNativeInstantView({ .source = &articleRelayoutSource, .mediaRuntime = articleRelayoutRuntime, }); Check( articleRelayoutPrepared.supported(), channelRelayoutLabel + u" article prepare supported"_q, ok); if (articleRelayoutPrepared.supported() && (joinedLayoutHeight > 0)) { auto articleRelayoutHeight = 0; auto articleRelayout = BuildArticleForTest( std::move(articleRelayoutPrepared.content), renderer, 320, &articleRelayoutHeight); const auto articleJoinBounds = HitBoundsWhere( articleRelayout.get(), 320, articleRelayoutHeight, lookupFlags, [](const MarkdownArticleHitTestResult &hit) { return hit.mediaActivation.kind == MediaActivationKind::JoinChannel; }); Check( articleJoinBounds.has_value(), channelRelayoutLabel + u" visible layout has join bounds"_q, ok); articleRelayoutChannelRuntime->joinVisibleValue = false; articleRelayout->invalidateLayout(); articleRelayoutHeight = articleRelayout->resizeGetHeight(320); const auto articleJoinedBounds = HitBoundsWhere( articleRelayout.get(), 320, articleRelayoutHeight, lookupFlags, [](const MarkdownArticleHitTestResult &hit) { return hit.mediaActivation.kind == MediaActivationKind::JoinChannel; }); Check( !articleJoinedBounds.has_value(), channelRelayoutLabel + u" invalidated relayout hides join bounds"_q, ok); Check( articleRelayoutHeight == joinedLayoutHeight, channelRelayoutLabel + u" invalidated relayout matches hidden layout"_q, ok); } auto previewRelayoutRuntime = std::make_shared(); auto previewRelayoutChannelRuntime = std::make_shared(); previewRelayoutRuntime->addChannelRuntime( relayoutChannelId, relayoutChannelUsername, previewRelayoutChannelRuntime); auto previewRelayoutSource = NativeIvSource(QVector{ relayoutChannelBlock, }); auto previewRelayoutPrepared = TryPrepareNativeInstantView({ .source = &previewRelayoutSource, .mediaRuntime = previewRelayoutRuntime, }); Check( previewRelayoutPrepared.supported(), channelRelayoutLabel + u" preview prepare supported"_q, ok); if (previewRelayoutPrepared.supported() && (joinedLayoutHeight > 0)) { auto window = Ui::RpWindow(); window.setGeometry(QRect(0, 0, 320, 320)); window.show(); FlushPendingWidgetEvents(); auto preview = CreateMarkdownPreviewWidget( window.body(), std::move(previewRelayoutPrepared.content), renderer, [](Event) { }); preview->setGeometry(QRect(QPoint(), window.body()->size())); preview->show(); FlushPendingWidgetEvents(); const auto body = FindChildObject( preview.get()); Check( body != nullptr, channelRelayoutLabel + u" preview body widget"_q, ok); if (body) { auto counter = WidgetEventCounter(); body->installEventFilter(&counter); const auto beforeHeight = body->height(); counter.reset(); previewRelayoutChannelRuntime->joinVisibleValue = false; previewRelayoutRuntime->fireChannelJoinedChange(relayoutChannelId); FlushPendingWidgetEvents(); const auto repainted = (counter.paintCount > 0) || (counter.updateRequestCount > 0); const auto afterHeight = body->height(); Check( beforeHeight > joinedLayoutHeight, channelRelayoutLabel + u" join visible height larger"_q, ok); Check( afterHeight == joinedLayoutHeight, channelRelayoutLabel + u" joined change relayout matches hidden layout"_q, ok); Check( repainted, channelRelayoutLabel + u" joined change triggers preview repaint"_q, ok); body->removeEventFilter(&counter); } } const auto collageLabel = u"native-iv-collage-article"_q; auto collageRuntime = std::make_shared(); auto collagePhotoRuntime = std::make_shared(); collagePhotoRuntime->thumbnailImage = std::make_shared(); collagePhotoRuntime->fullImage = std::make_shared(); auto collageDocumentRuntime = std::make_shared(); collageDocumentRuntime->thumbnailImage = std::make_shared(); collageDocumentRuntime->fullImage = std::make_shared(); collageRuntime->addPhotoRuntime(9102, collagePhotoRuntime); collageRuntime->addDocumentRuntime(7003, collageDocumentRuntime); auto collageSource = NativeIvSource( QVector{ collageFixture.block }, collageFixture.photos, collageFixture.documents); auto collagePrepared = TryPrepareNativeInstantView({ .source = &collageSource, .mediaRuntime = collageRuntime, }); Check( collagePrepared.supported(), collageLabel + u" prepare supported"_q, ok); if (collagePrepared.supported()) { auto collageHeight = 0; auto collageArticle = BuildArticleForTest( std::move(collagePrepared.content), renderer, 460, &collageHeight); const auto collageImage = PaintArticleForTest( collageArticle.get(), 460, collageHeight); Check( HasPaintedPixels(collageImage), collageLabel + u" paint produced pixels"_q, ok); Check( collageRuntime->photoRequests.size() == 1 && (collageRuntime->photoRequests.front() == 9102), collageLabel + u" photo resolve request"_q, ok); Check( collageRuntime->documentRequests.size() == 1 && (collageRuntime->documentRequests.front() == 7003), collageLabel + u" document resolve request"_q, ok); const auto collagePhotoBounds = HitBoundsWhere( collageArticle.get(), 460, collageHeight, lookupFlags, [&](const MarkdownArticleHitTestResult &hit) { return hit.mediaActivation.kind == MediaActivationKind::Photo && hit.mediaActivation.photo.get() == collagePhotoRuntime.get(); }); const auto collageVideoBounds = HitBoundsWhere( collageArticle.get(), 460, collageHeight, lookupFlags, [&](const MarkdownArticleHitTestResult &hit) { return hit.mediaActivation.kind == MediaActivationKind::Document && hit.mediaActivation.document.get() == collageDocumentRuntime.get(); }); Check( collagePhotoBounds.has_value(), collageLabel + u" photo item bounds"_q, ok); Check( collageVideoBounds.has_value(), collageLabel + u" video item bounds"_q, ok); if (collagePhotoBounds) { const auto hit = collageArticle->hitTest( collagePhotoBounds->center(), lookupFlags); if (hit.mediaActivation.photo) { hit.mediaActivation.photo->open(Qt::LeftButton); } const auto context = collageArticle->textForContext(hit); Check( context.expanded == collageFixture.caption, collageLabel + u" context export text"_q, ok); } if (collageVideoBounds) { const auto hit = collageArticle->hitTest( collageVideoBounds->center(), lookupFlags); if (hit.mediaActivation.document) { hit.mediaActivation.document->open(Qt::LeftButton); } } Check( collagePhotoRuntime->openedButtons.size() == 1, collageLabel + u" photo open intent"_q, ok); Check( collageDocumentRuntime->openedButtons.size() == 1, collageLabel + u" video open intent"_q, ok); const auto collageSelection = collageArticle->textForSelection({ .from = { .segment = 0, .offset = 0 }, .to = { .segment = 0, .offset = 1 }, }, nullptr); Check( collageSelection.expanded == collageFixture.caption, collageLabel + u" selection export text"_q, ok); const auto collageCaptionSelection = collageArticle->textForSelection({ .from = { .segment = 1, .offset = 0 }, .to = { .segment = 1, .offset = collageArticle->segmentLength(1), }, }, nullptr); Check( collageCaptionSelection.expanded == collageFixture.caption, collageLabel + u" caption selection export"_q, ok); } const auto slideshowLabel = u"native-iv-slideshow-article"_q; auto slideshowRuntime = std::make_shared(); auto slideshowDocumentA = std::make_shared(); slideshowDocumentA->thumbnailImage = std::make_shared(); slideshowDocumentA->fullImage = std::make_shared(); auto slideshowDocumentB = std::make_shared(); slideshowDocumentB->thumbnailImage = std::make_shared(); slideshowDocumentB->fullImage = std::make_shared(); slideshowRuntime->addDocumentRuntime(7004, slideshowDocumentA); slideshowRuntime->addDocumentRuntime(7005, slideshowDocumentB); auto slideshowSource = NativeIvSource( QVector{ slideshowFixture.block }, slideshowFixture.photos, slideshowFixture.documents); auto slideshowPrepared = TryPrepareNativeInstantView({ .source = &slideshowSource, .mediaRuntime = slideshowRuntime, }); Check( slideshowPrepared.supported(), slideshowLabel + u" prepare supported"_q, ok); if (slideshowPrepared.supported()) { auto slideshowHeight = 0; auto slideshowArticle = BuildArticleForTest( std::move(slideshowPrepared.content), renderer, 460, &slideshowHeight); const auto slideshowImage = PaintArticleForTest( slideshowArticle.get(), 460, slideshowHeight); Check( HasPaintedPixels(slideshowImage), slideshowLabel + u" paint produced pixels"_q, ok); Check( slideshowRuntime->documentRequests.size() == 2, slideshowLabel + u" document resolve request count"_q, ok); const auto slideshowItemBounds = HitBoundsWhere( slideshowArticle.get(), 460, slideshowHeight, lookupFlags, [&](const MarkdownArticleHitTestResult &hit) { return hit.mediaActivation.kind == MediaActivationKind::Document && hit.mediaActivation.document.get() == slideshowDocumentA.get(); }); Check( slideshowItemBounds.has_value(), slideshowLabel + u" document item bounds"_q, ok); if (slideshowItemBounds) { const auto hit = slideshowArticle->hitTest( slideshowItemBounds->center(), lookupFlags); if (hit.mediaActivation.document) { hit.mediaActivation.document->open(Qt::LeftButton); } const auto context = slideshowArticle->textForContext(hit); Check( context.expanded == (tr::lng_media_selected_video(tr::now, lt_count, 2) + u"\n"_q + slideshowFixture.caption), slideshowLabel + u" context export text"_q, ok); } const auto slideshowSelection = slideshowArticle->textForSelection({ .from = { .segment = 0, .offset = 0 }, .to = { .segment = 0, .offset = 1 }, }, nullptr); Check( slideshowSelection.expanded == (tr::lng_media_selected_video(tr::now, lt_count, 2) + u"\n"_q + slideshowFixture.caption), slideshowLabel + u" selection export text"_q, ok); const auto slideshowCaptionSelection = slideshowArticle->textForSelection({ .from = { .segment = 1, .offset = 0 }, .to = { .segment = 1, .offset = slideshowArticle->segmentLength(1), }, }, nullptr); Check( slideshowCaptionSelection.expanded == slideshowFixture.caption, slideshowLabel + u" caption selection export"_q, ok); Check( slideshowDocumentA->openedButtons.size() == 1, slideshowLabel + u" document open intent"_q, ok); } } void CheckPrepareCoverage( const PreparedFixture &markdownFixture, const PreparedFixture &latexFixture, bool *ok) { const auto &markdown = markdownFixture.prepared; const auto &latex = latexFixture.prepared; auto markdownTables = std::vector(); const PreparedBlock *markdownDisplayMath = nullptr; const PreparedBlock *detailsBlock = nullptr; auto footnoteDefinitionListFound = false; auto footnoteDefinitionAnchorFound = false; const auto isFootnoteDefinitionAnchor = [](const QString &anchorId) { return anchorId == FromLatin1("fn-1") || anchorId == FromLatin1("fn-2"); }; ForEachPreparedBlock(markdown.blocks.blocks, [&](const PreparedBlock &block) { if (block.kind == PreparedBlockKind::Table) { markdownTables.push_back(&block); } if (!markdownDisplayMath && block.kind == PreparedBlockKind::DisplayMath && block.formulaTex.trimmed() == FromLatin1("\\int_0^1 x^2\\,dx = \\frac{1}{3}")) { markdownDisplayMath = █ } if (!detailsBlock && block.kind == PreparedBlockKind::Details) { detailsBlock = █ } if (isFootnoteDefinitionAnchor(block.anchorId)) { footnoteDefinitionAnchorFound = true; } if (block.kind == PreparedBlockKind::List) { for (const auto &child : block.children) { if (isFootnoteDefinitionAnchor(child.anchorId)) { footnoteDefinitionListFound = true; } } } }); Check( markdownDisplayMath != nullptr, FromLatin1("markdown-example.md prepared display math formulaTex"), ok); Check( markdownTables.size() >= 2, FromLatin1("markdown-example.md prepared table count"), ok); if (markdownTables.size() >= 2) { const auto &firstTable = *markdownTables[0]; const auto &secondTable = *markdownTables[1]; Check( firstTable.tableColumnCount == 3, FromLatin1("markdown-example.md prepared first table column count"), ok); Check( firstTable.tableRows.size() == 4, FromLatin1("markdown-example.md prepared first table row count"), ok); if (firstTable.tableRows.size() == 4) { Check( firstTable.tableRows[0].header, FromLatin1("markdown-example.md prepared first table header row"), ok); Check( firstTable.tableRows[0].cells.size() == 3 && firstTable.tableRows[1].cells.size() == 3, FromLatin1("markdown-example.md prepared first table cell shape"), ok); } Check( secondTable.tableAlignments.size() == 3 && secondTable.tableAlignments[0] == TableAlignment::Left && secondTable.tableAlignments[1] == TableAlignment::Center && secondTable.tableAlignments[2] == TableAlignment::Right, FromLatin1("markdown-example.md prepared second table alignments"), ok); } Check( detailsBlock != nullptr, FromLatin1("markdown-example.md prepared details block"), ok); if (detailsBlock) { Check( detailsBlock->collapsed, FromLatin1("markdown-example.md prepared details collapsed"), ok); Check( detailsBlock->anchorId.startsWith(FromLatin1("details-")), FromLatin1("markdown-example.md prepared details anchor id"), ok); Check( detailsBlock->text.text == FromLatin1("Click to expand details/summary block"), FromLatin1("markdown-example.md prepared details summary text"), ok); Check( detailsBlock->links.empty() && !HasEntityType( detailsBlock->text.entities, EntityType::CustomUrl), FromLatin1("markdown-example.md prepared details summary toggle"), ok); Check( !detailsBlock->children.empty() && detailsBlock->children[0].kind == PreparedBlockKind::Paragraph && detailsBlock->children[0].text.text.contains( FromLatin1("Hidden content inside details.")), FromLatin1("markdown-example.md prepared details body"), ok); } Check( !footnoteDefinitionListFound, FromLatin1("markdown-example.md no prepared footnote list"), ok); Check( !footnoteDefinitionAnchorFound, FromLatin1("markdown-example.md no prepared footnote anchors"), ok); const auto footnoteOne = FindPreparedFootnote(markdown, FromLatin1("1")); const auto longFootnote = FindPreparedFootnote( markdown, FromLatin1("long-note")); Check( footnoteOne != nullptr && footnoteOne->displayText == FromLatin1("[1]"), FromLatin1("markdown-example.md prepared footnote one display"), ok); Check( longFootnote != nullptr && longFootnote->displayText == FromLatin1("[long-note]"), FromLatin1("markdown-example.md prepared long footnote display"), ok); auto normalFootnoteBacklinkFound = false; ForEachPreparedLink(markdown.blocks.blocks, [&](const PreparedLink &link) { if (link.kind == PreparedLinkKind::FootnoteBacklink) { normalFootnoteBacklinkFound = true; } }); auto preparedFootnoteBacklinkFound = false; ForEachPreparedFootnoteLink(markdown, [&](const PreparedLink &link) { if (link.kind == PreparedLinkKind::FootnoteBacklink) { preparedFootnoteBacklinkFound = true; } }); Check( !normalFootnoteBacklinkFound, FromLatin1("markdown-example.md no normal footnote backlink"), ok); Check( !preparedFootnoteBacklinkFound, FromLatin1("markdown-example.md no prepared footnote backlink"), ok); const auto firstReferenceText = FromLatin1("[1]"); const auto firstReferenceParagraph = FindPreparedParagraphContaining( markdown, FromLatin1("Footnote reference one.")); Check( firstReferenceParagraph != nullptr, FromLatin1("markdown-example.md first prepared footnote paragraph"), ok); if (firstReferenceParagraph) { const auto &text = firstReferenceParagraph->text; Check( text.text.contains(FromLatin1("Footnote reference one.[1]")), FromLatin1("markdown-example.md first footnote display text"), ok); const auto offset = text.text.indexOf(firstReferenceText); Check( offset >= 0, FromLatin1("markdown-example.md first footnote text range"), ok); if (offset >= 0) { const auto length = firstReferenceText.size(); Check( HasEntityRange(text, EntityType::Superscript, offset, length), FromLatin1("markdown-example.md first footnote superscript"), ok); Check( HasEntityRange(text, EntityType::CustomUrl, offset, length), FromLatin1("markdown-example.md first footnote custom url"), ok); const auto link = FindPreparedLinkByCustomUrlRange( text, firstReferenceParagraph->links, offset, length); Check( link != nullptr && link->kind == PreparedLinkKind::Footnote && link->target == FromLatin1("1") && link->copyText == FromLatin1("[1]"), FromLatin1("markdown-example.md first footnote prepared link"), ok); } } const auto secondReferenceText = FromLatin1("[long-note]"); const auto secondReferenceParagraph = FindPreparedParagraphContaining( markdown, FromLatin1("Footnote reference two with more text.")); Check( secondReferenceParagraph != nullptr, FromLatin1("markdown-example.md second prepared footnote paragraph"), ok); if (secondReferenceParagraph) { const auto &text = secondReferenceParagraph->text; Check( text.text.contains(secondReferenceText), FromLatin1("markdown-example.md second footnote display text"), ok); Check( !text.text.contains(FromLatin1("[2]")), FromLatin1("markdown-example.md second footnote no ordinal display"), ok); const auto offset = text.text.indexOf(secondReferenceText); Check( offset >= 0, FromLatin1("markdown-example.md second footnote text range"), ok); if (offset >= 0) { const auto length = secondReferenceText.size(); Check( HasEntityRange(text, EntityType::Superscript, offset, length), FromLatin1("markdown-example.md second footnote superscript"), ok); Check( HasEntityRange(text, EntityType::CustomUrl, offset, length), FromLatin1("markdown-example.md second footnote custom url"), ok); const auto link = FindPreparedLinkByCustomUrlRange( text, secondReferenceParagraph->links, offset, length); Check( link != nullptr && link->kind == PreparedLinkKind::Footnote && link->target == FromLatin1("long-note") && link->copyText == FromLatin1("[long-note]"), FromLatin1("markdown-example.md second footnote prepared link"), ok); } } if (longFootnote) { const auto &text = longFootnote->text; Check( text.text.contains(FromLatin1( "This is a longer footnote with formatting, code, and a link.")), FromLatin1("markdown-example.md long footnote text"), ok); Check( HasEntityType(text.entities, EntityType::Bold), FromLatin1("markdown-example.md long footnote bold entity"), ok); Check( HasEntityType(text.entities, EntityType::Code), FromLatin1("markdown-example.md long footnote code entity"), ok); Check( HasEntityType(text.entities, EntityType::CustomUrl), FromLatin1("markdown-example.md long footnote custom url entity"), ok); auto externalLinkFound = false; for (const auto &link : longFootnote->links) { if (link.kind == PreparedLinkKind::External && (link.target.contains(FromLatin1("https://example.com")) || link.copyText.contains( FromLatin1("https://example.com")))) { externalLinkFound = true; } } Check( externalLinkFound, FromLatin1("markdown-example.md long footnote external link"), ok); } auto latexTables = std::vector(); ForEachPreparedBlock(latex.blocks.blocks, [&](const PreparedBlock &block) { if (block.kind == PreparedBlockKind::Table) { latexTables.push_back(&block); } }); Check( !latexTables.empty(), FromLatin1("latex-markdown-test.md prepared table count"), ok); if (!latexTables.empty()) { const auto &table = *latexTables[0]; Check( table.tableColumnCount == 3, FromLatin1("latex-markdown-test.md prepared table column count"), ok); Check( table.tableRows.size() == 5, FromLatin1("latex-markdown-test.md prepared table row count"), ok); if (table.tableRows.size() == 5) { Check( table.tableRows[0].header, FromLatin1("latex-markdown-test.md prepared table header row"), ok); Check( table.tableRows[1].cells.size() == 3, FromLatin1("latex-markdown-test.md prepared table cell count"), ok); } } const auto &limits = PrepareTableRenderLimitsForIv(); auto overflowTable = QByteArray("| A | B |\n| --- | --- |\n"); for (auto i = 0; i != limits.maxRows; ++i) { overflowTable.append("| row "); overflowTable.append(QByteArray::number(i)); overflowTable.append(" | value |\n"); } const auto overflowLabel = FromLatin1("generated-overflow-table.md"); const auto overflowParsed = ParseMarkdownForIv( overflowTable, ParseOptions{ overflowLabel }); Check( overflowParsed.ok, overflowLabel + FromLatin1(" parse failed: ") + overflowParsed.error, ok); if (overflowParsed.ok) { const auto overflowPrepared = PrepareParsedDocumentForTest( overflowParsed.document, overflowLabel, std::make_shared()); Check( !overflowPrepared.failure.failed(), overflowLabel + FromLatin1(" prepare failure: ") + PrepareFailureReason(overflowPrepared.failure), ok); auto overflowTableBlocks = 0; ForEachPreparedBlock( overflowPrepared.blocks.blocks, [&](const PreparedBlock &block) { if (block.kind == PreparedBlockKind::Table) { ++overflowTableBlocks; } }); Check( overflowPrepared.debug.prepareWarningCount > 0, overflowLabel + FromLatin1(" flatten warning count"), ok); Check( overflowTableBlocks == 0, overflowLabel + FromLatin1(" flattened table block removed"), ok); Check( !overflowPrepared.blocks.blocks.empty(), overflowLabel + FromLatin1(" flattened fallback blocks present"), ok); } } void CheckPrepareLinkClassification( const QString &sourcePath, bool *ok) { const auto label = FromLatin1("generated-relative-links.md"); const auto parsed = ParseMarkdownForIv( QByteArray( "[Local](./docs/getting-started.md#section-1)\n" "[Rejected](../outside.md)\n"), ParseOptions{ label }); Check( parsed.ok, label + FromLatin1(" parse failed: ") + parsed.error, ok); if (!parsed.ok) { return; } const auto prepared = PrepareParsedDocumentForTest( parsed.document, sourcePath, std::make_shared()); Check( !prepared.failure.failed(), label + FromLatin1(" prepare failure: ") + PrepareFailureReason(prepared.failure), ok); const auto expectedLocalTarget = QDir( QFileInfo(sourcePath).absolutePath()).absoluteFilePath( FromLatin1("docs/getting-started.md")); auto foundLocal = false; auto foundRejected = false; ForEachPreparedLink(prepared.blocks.blocks, [&](const PreparedLink &link) { if (link.kind == PreparedLinkKind::LocalFile && link.target == QDir::cleanPath(expectedLocalTarget) && link.fragment == FromLatin1("section-1") && link.copyText == FromLatin1("./docs/getting-started.md#section-1")) { foundLocal = true; } if (link.kind == PreparedLinkKind::RejectedRelative && link.copyText == FromLatin1("../outside.md")) { foundRejected = true; } }); Check( foundLocal, FromLatin1("generated-relative-links.md local markdown classification"), ok); Check( foundRejected, FromLatin1("generated-relative-links.md rejected relative classification"), ok); } void CheckPreparedExternalLinkCoverage(bool *ok) { const auto markdownLabel = FromLatin1("generated-prepared-external-links.md"); const auto markdownParsed = ParseMarkdownForIv( QByteArray( "[https://visible.example.com/path?q=1](https://visible.example.com/path?q=1)\n" "[partial-visible.example.com/path?q=3](https://partial-visible.example.com/path?q=3)\n" "[Hidden label](https://hidden.example.com/path?q=2)\n" "[support@example.com](mailto:support@example.com?subject=Hello%20Telegram)\n"), ParseOptions{ markdownLabel }); Check( markdownParsed.ok, markdownLabel + FromLatin1(" parse failed: ") + markdownParsed.error, ok); if (markdownParsed.ok) { const auto markdownPrepared = PrepareParsedDocumentForTest( markdownParsed.document, markdownLabel, std::make_shared()); Check( !markdownPrepared.failure.failed(), markdownLabel + FromLatin1(" prepare failure: ") + PrepareFailureReason(markdownPrepared.failure), ok); if (!markdownPrepared.failure.failed()) { const auto visibleTarget = UrlClickHandler::EncodeForOpening( FromLatin1("https://visible.example.com/path?q=1")); const auto partialTarget = UrlClickHandler::EncodeForOpening( FromLatin1("https://partial-visible.example.com/path?q=3")); const auto hiddenTarget = UrlClickHandler::EncodeForOpening( FromLatin1("https://hidden.example.com/path?q=2")); const auto emailTarget = FromLatin1("support@example.com"); const auto markdownVisible = FindPreparedLink( markdownPrepared.blocks.blocks, [&](const PreparedLink &link) { return link.kind == PreparedLinkKind::External && link.target == visibleTarget; }); const auto markdownHidden = FindPreparedLink( markdownPrepared.blocks.blocks, [&](const PreparedLink &link) { return link.kind == PreparedLinkKind::External && link.target == hiddenTarget; }); const auto markdownPartial = FindPreparedLink( markdownPrepared.blocks.blocks, [&](const PreparedLink &link) { return link.kind == PreparedLinkKind::External && link.target == partialTarget; }); const auto markdownEmail = FindPreparedLink( markdownPrepared.blocks.blocks, [&](const PreparedLink &link) { return link.kind == PreparedLinkKind::External && link.target == emailTarget; }); Check( markdownVisible != nullptr && markdownVisible->entityType == EntityType::Url && markdownVisible->copyText == visibleTarget && markdownVisible->shown == EntityLinkShown::Full, markdownLabel + FromLatin1(" visible URL semantics"), ok); Check( markdownPartial != nullptr && markdownPartial->entityType == EntityType::Url && markdownPartial->copyText == partialTarget && markdownPartial->shown == EntityLinkShown::Partial, markdownLabel + FromLatin1(" partial visible URL semantics"), ok); Check( markdownHidden != nullptr && markdownHidden->entityType == EntityType::CustomUrl && markdownHidden->copyText == hiddenTarget && markdownHidden->shown == EntityLinkShown::Full, markdownLabel + FromLatin1(" hidden URL semantics"), ok); Check( markdownEmail != nullptr && markdownEmail->entityType == EntityType::Email && markdownEmail->copyText == emailTarget && markdownEmail->target == emailTarget && !markdownEmail->target.contains(QChar(':')), markdownLabel + FromLatin1(" email target normalization"), ok); } } const auto nativeLabel = u"native-iv-prepared-external-links"_q; auto nativeBlocks = QVector(); nativeBlocks.push_back(MTP_pageBlockParagraph(NativeIvConcat({ NativeIvTextUrl( u"https://native-visible.example/path?q=1"_q, u"https://native-visible.example/path?q=1"_q), NativeIvText(u" "_q), NativeIvTextUrl( u"native-partial.example/path?q=3"_q, u"https://native-partial.example/path?q=3"_q), NativeIvText(u" "_q), NativeIvTextUrl( u"Native hidden label"_q, u"https://native-hidden.example/path?q=2"_q), NativeIvText(u" "_q), NativeIvTextEmail(u"native@example.com"_q, u"native@example.com"_q), }))); auto nativeSource = NativeIvSource(std::move(nativeBlocks)); const auto nativePrepared = TryPrepareNativeInstantView({ .source = &nativeSource, }); Check( nativePrepared.supported(), nativeLabel + u" prepare supported"_q, ok); Check( !nativePrepared.content.failure.failed(), nativeLabel + u" prepare failure"_q, ok); if (nativePrepared.supported() && !nativePrepared.content.failure.failed()) { const auto visibleTarget = UrlClickHandler::EncodeForOpening( FromLatin1("https://native-visible.example/path?q=1")); const auto partialTarget = UrlClickHandler::EncodeForOpening( FromLatin1("https://native-partial.example/path?q=3")); const auto hiddenTarget = UrlClickHandler::EncodeForOpening( FromLatin1("https://native-hidden.example/path?q=2")); const auto emailTarget = FromLatin1("native@example.com"); const auto nativeVisible = FindPreparedLink( nativePrepared.content.blocks.blocks, [&](const PreparedLink &link) { return link.kind == PreparedLinkKind::External && link.target == visibleTarget; }); const auto nativeHidden = FindPreparedLink( nativePrepared.content.blocks.blocks, [&](const PreparedLink &link) { return link.kind == PreparedLinkKind::External && link.target == hiddenTarget; }); const auto nativePartial = FindPreparedLink( nativePrepared.content.blocks.blocks, [&](const PreparedLink &link) { return link.kind == PreparedLinkKind::External && link.target == partialTarget; }); const auto nativeEmail = FindPreparedLink( nativePrepared.content.blocks.blocks, [&](const PreparedLink &link) { return link.kind == PreparedLinkKind::External && link.target == emailTarget; }); Check( nativeVisible != nullptr && nativeVisible->entityType == EntityType::Url && nativeVisible->copyText == visibleTarget && nativeVisible->shown == EntityLinkShown::Full, nativeLabel + u" visible URL semantics"_q, ok); Check( nativePartial != nullptr && nativePartial->entityType == EntityType::Url && nativePartial->copyText == partialTarget && nativePartial->shown == EntityLinkShown::Partial, nativeLabel + u" partial visible URL semantics"_q, ok); Check( nativeHidden != nullptr && nativeHidden->entityType == EntityType::CustomUrl && nativeHidden->copyText == hiddenTarget && nativeHidden->shown == EntityLinkShown::Full, nativeLabel + u" hidden URL semantics"_q, ok); Check( nativeEmail != nullptr && nativeEmail->entityType == EntityType::Email && nativeEmail->copyText == emailTarget && nativeEmail->target == emailTarget, nativeLabel + u" email semantics"_q, ok); } } void CheckDetailsSummaryHitCoverage(bool *ok) { const auto label = FromLatin1("generated-details-summary-hit.md"); const auto parsed = ParseMarkdownForIv( QByteArray( "
\n" "Summary text for selection\n" "\n" "Hidden body.\n" "
\n"), ParseOptions{ label }); Check( parsed.ok, label + FromLatin1(" parse failed: ") + parsed.error, ok); if (!parsed.ok) { return; } const auto prepared = PrepareParsedDocumentForTest( parsed.document, label, std::make_shared()); Check( !prepared.failure.failed(), label + FromLatin1(" prepare failure: ") + PrepareFailureReason(prepared.failure), ok); if (prepared.failure.failed()) { return; } auto height = 0; auto article = BuildArticleForTest( prepared, std::make_shared(), 420, &height); const auto bounds = SegmentHitBounds(article.get(), 420, height, 0); Check( bounds.has_value(), label + FromLatin1(" details segment hit bounds"), ok); Check( article->segmentIsText(0), label + FromLatin1(" details segment remains text-selectable"), ok); if (!bounds || !article->segmentIsText(0)) { return; } auto flags = Ui::Text::StateRequest::Flags(); flags |= Ui::Text::StateRequest::Flag::LookupLink; flags |= Ui::Text::StateRequest::Flag::LookupSymbol; auto summaryTextHit = false; auto summaryPaddingHit = false; for (auto y = bounds->top(); (y <= bounds->bottom()) && !(summaryTextHit && summaryPaddingHit); ++y) { for (auto x = bounds->left(); (x <= bounds->right()) && !(summaryTextHit && summaryPaddingHit); ++x) { const auto hit = article->hitTest(QPoint(x, y), flags); if (!hit.valid() || !hit.direct || (hit.segmentIndex != 0) || !hit.preparedLink || (hit.preparedLink->kind != PreparedLinkKind::ToggleDetails)) { continue; } if (hit.state.link) { summaryPaddingHit = true; } else { summaryTextHit = true; } } } Check( summaryTextHit, label + FromLatin1(" summary text hit leaves link inactive"), ok); Check( summaryPaddingHit, label + FromLatin1(" summary padding hit keeps toggle link"), ok); } void CheckTableFormulaTextSizeCoverage(bool *ok) { const auto label = FromLatin1("generated-table-formula-text-sizes.md"); const auto parsed = ParseMarkdownForIv( QByteArray( "| Header $x$ | Value |\n" "| --- | --- |\n" "| Body $y$ | Value |\n"), ParseOptions{ label }); Check( parsed.ok, label + FromLatin1(" parse failed: ") + parsed.error, ok); if (!parsed.ok) { return; } auto dimensions = CaptureMarkdownPrepareDimensions(); dimensions.bodyTextSize = 11; dimensions.tableHeaderTextSize = 29; dimensions.tableBodyTextSize = 17; const auto prepared = PrepareParsedDocumentForTest( parsed.document, label, std::make_shared(), dimensions); Check( !prepared.failure.failed(), label + FromLatin1(" prepare failure: ") + PrepareFailureReason(prepared.failure), ok); if (prepared.failure.failed()) { return; } const auto headerFormula = FindPreparedFormulaSlot( prepared, FromLatin1("x"), MathKind::Inline); const auto bodyFormula = FindPreparedFormulaSlot( prepared, FromLatin1("y"), MathKind::Inline); Check( headerFormula != nullptr, label + FromLatin1(" header formula slot"), ok); Check( bodyFormula != nullptr, label + FromLatin1(" body formula slot"), ok); if (headerFormula && bodyFormula) { Check( headerFormula->textSize == dimensions.tableHeaderTextSize, label + FromLatin1(" header formula text size"), ok); Check( bodyFormula->textSize == dimensions.tableBodyTextSize, label + FromLatin1(" body formula text size"), ok); Check( headerFormula->textSize != bodyFormula->textSize, label + FromLatin1(" header/body formula size split"), ok); Check( headerFormula->textSize != dimensions.bodyTextSize && bodyFormula->textSize != dimensions.bodyTextSize, label + FromLatin1(" table formulas avoid body text size"), ok); } } void CheckInlineTextObjectArticleCoverage(bool *ok) { const auto selectionLabel = FromLatin1("generated-inline-selection.md"); const auto selectionParsed = ParseMarkdownForIv( QByteArray("Inline formula $\\frac{a}{b}$ export.\n"), ParseOptions{ selectionLabel }); Check( selectionParsed.ok, selectionLabel + FromLatin1(" parse failed: ") + selectionParsed.error, ok); if (selectionParsed.ok) { auto selectionRenderer = std::make_shared(); auto selectionPrepared = PrepareParsedDocumentForTest( selectionParsed.document, selectionLabel, selectionRenderer); Check( !selectionPrepared.failure.failed(), selectionLabel + FromLatin1(" prepare failure: ") + PrepareFailureReason(selectionPrepared.failure), ok); if (!selectionPrepared.failure.failed()) { auto selectionObjectOffset = -1; ForEachPreparedBlock( selectionPrepared.blocks.blocks, [&](const PreparedBlock &block) { if (selectionObjectOffset >= 0) { return; } const auto matches = CollectInlineTextObjectMatches(block.text); if (!matches.empty()) { selectionObjectOffset = matches.front().entity.offset(); } }); auto selectionFormulaWidth = 0; for (const auto &slot : selectionPrepared.formulas) { if (slot.present && (slot.kind == MathKind::Inline) && slot.measured.success) { selectionFormulaWidth = std::max( slot.measured.logicalSize.width(), 1); break; } } auto selectionArticleHeight = 0; auto selectionArticle = BuildArticleForTest( std::move(selectionPrepared), selectionRenderer, 480, &selectionArticleHeight); const auto selectionImage = PaintArticleForTest( selectionArticle.get(), 480, selectionArticleHeight); Check( HasPaintedPixels(selectionImage), selectionLabel + FromLatin1(" paint produced pixels"), ok); const auto selectionHitBounds = SymbolHitBounds( selectionArticle.get(), 480, selectionArticleHeight, selectionObjectOffset); Check( selectionHitBounds.has_value(), selectionLabel + FromLatin1(" inline formula hit bounds"), ok); if (selectionHitBounds && (selectionFormulaWidth > 0)) { Check( selectionHitBounds->width() >= selectionFormulaWidth, selectionLabel + FromLatin1( " inline formula hit width uses object width"), ok); } Check( selectionArticle->segmentIsText(0), selectionLabel + FromLatin1(" first segment is text"), ok); if (selectionArticle->segmentIsText(0)) { const auto exported = selectionArticle->textForSelection({ .from = { .segment = 0, .offset = 0 }, .to = { .segment = 0, .offset = selectionArticle->segmentLength(0), }, }, nullptr); const auto expected = FromLatin1( "Inline formula $\\frac{a}{b}$ export."); Check( exported.expanded == expected, selectionLabel + FromLatin1(" expanded export"), ok); Check( exported.rich.text == expected, selectionLabel + FromLatin1(" rich export text"), ok); Check( !HasEntityType(exported.rich.entities, EntityType::CustomEmoji), selectionLabel + FromLatin1(" export drops custom emoji entity"), ok); } } } const auto repeatedLabel = FromLatin1("generated-repeated-inline-formulas.md"); const auto repeatedParsed = ParseMarkdownForIv( QByteArray("Repeated $x^2$ then $x^2$ then $x^2$.\n"), ParseOptions{ repeatedLabel }); Check( repeatedParsed.ok, repeatedLabel + FromLatin1(" parse failed: ") + repeatedParsed.error, ok); if (!repeatedParsed.ok) { return; } auto repeatedRenderer = std::make_shared(); auto repeatedPrepared = PrepareParsedDocumentForTest( repeatedParsed.document, repeatedLabel, repeatedRenderer); Check( !repeatedPrepared.failure.failed(), repeatedLabel + FromLatin1(" prepare failure: ") + PrepareFailureReason(repeatedPrepared.failure), ok); if (repeatedPrepared.failure.failed()) { return; } const auto repeatedPointers = MeasuredFormulaPointers(repeatedPrepared); Check( repeatedPointers.size() == 3, repeatedLabel + FromLatin1(" measured pointer count"), ok); if (repeatedPointers.size() == 3) { Check( repeatedPointers[0] == repeatedPointers[1] && repeatedPointers[1] == repeatedPointers[2], repeatedLabel + FromLatin1(" measured pointer reuse in one prepare"), ok); } auto repeatedArticleHeight = 0; auto repeatedArticle = BuildArticleForTest( std::move(repeatedPrepared), repeatedRenderer, 480, &repeatedArticleHeight); const auto repeatedFirstImage = PaintArticleForTest( repeatedArticle.get(), 480, repeatedArticleHeight); Check( HasPaintedPixels(repeatedFirstImage), repeatedLabel + FromLatin1(" first paint produced pixels"), ok); const auto repeatedFirstCounters = repeatedRenderer->debugCounters(); Check( repeatedFirstCounters.misses >= 1, repeatedLabel + FromLatin1(" first paint raster misses"), ok); Check( repeatedFirstCounters.rendered >= 1, repeatedLabel + FromLatin1(" first paint raster rendered"), ok); const auto repeatedSecondImage = PaintArticleForTest( repeatedArticle.get(), 480, repeatedArticleHeight); Check( HasPaintedPixels(repeatedSecondImage), repeatedLabel + FromLatin1(" second paint produced pixels"), ok); const auto repeatedSecondCounters = repeatedRenderer->debugCounters(); Check( repeatedSecondCounters.hits == repeatedFirstCounters.hits && repeatedSecondCounters.misses == repeatedFirstCounters.misses && repeatedSecondCounters.rendered == repeatedFirstCounters.rendered, repeatedLabel + FromLatin1(" same article paint cache reuse"), ok); repeatedRenderer->resetDebugCounters(); auto repeatedPreparedAgain = PrepareParsedDocumentForTest( repeatedParsed.document, repeatedLabel, repeatedRenderer); Check( !repeatedPreparedAgain.failure.failed(), repeatedLabel + FromLatin1(" second prepare failure: ") + PrepareFailureReason(repeatedPreparedAgain.failure), ok); if (!repeatedPreparedAgain.failure.failed()) { auto repeatedArticleAgainHeight = 0; auto repeatedArticleAgain = BuildArticleForTest( std::move(repeatedPreparedAgain), repeatedRenderer, 480, &repeatedArticleAgainHeight); const auto repeatedCachedImage = PaintArticleForTest( repeatedArticleAgain.get(), 480, repeatedArticleAgainHeight); Check( HasPaintedPixels(repeatedCachedImage), repeatedLabel + FromLatin1(" cached paint produced pixels"), ok); const auto repeatedReuseCounters = repeatedRenderer->debugCounters(); Check( repeatedReuseCounters.hits >= 1, repeatedLabel + FromLatin1(" renderer cache reuse hits"), ok); Check( repeatedReuseCounters.misses == 0, repeatedLabel + FromLatin1(" renderer cache reuse misses"), ok); Check( repeatedReuseCounters.rendered == 0, repeatedLabel + FromLatin1(" renderer cache reuse rendered"), ok); } } void CheckArticleRasterRegressionCoverage(bool *ok) { struct SegmentTextProbe { int segmentIndex = -1; int offset = -1; int length = 0; TextWithEntities segment; [[nodiscard]] bool valid() const { return (segmentIndex >= 0) && (offset >= 0) && (length > 0); } }; const auto wideFormulaTex = [] { auto result = QByteArray(); for (auto i = 0; i != 24; ++i) { if (!result.isEmpty()) { result.append(" + "); } result.append("x_"); result.append(QByteArray::number(i)); } return result; }(); const auto forEachPreparedTextSegment = []( const MarkdownArticleContent &prepared, auto &&callback) { auto segmentIndex = 0; const auto collectTextSegment = [&](const TextWithEntities &text) { if (callback(segmentIndex, text)) { return true; } ++segmentIndex; return false; }; const auto visitBlock = [&](const auto &self, const PreparedBlock &block) -> bool { switch (block.kind) { case PreparedBlockKind::Paragraph: case PreparedBlockKind::Heading: case PreparedBlockKind::Details: case PreparedBlockKind::CodeBlock: if (collectTextSegment(block.text)) { return true; } break; case PreparedBlockKind::DisplayMath: ++segmentIndex; break; case PreparedBlockKind::Table: ++segmentIndex; for (const auto &row : block.tableRows) { for (const auto &cell : row.cells) { if (collectTextSegment(cell.text)) { return true; } } } break; case PreparedBlockKind::Placeholder: case PreparedBlockKind::Photo: ++segmentIndex; if (!block.text.text.isEmpty() && collectTextSegment(block.text)) { return true; } break; case PreparedBlockKind::Rule: case PreparedBlockKind::List: case PreparedBlockKind::ListItem: case PreparedBlockKind::Quote: break; } for (const auto &child : block.children) { if (self(self, child)) { return true; } } return false; }; for (const auto &block : prepared.blocks.blocks) { if (visitBlock(visitBlock, block)) { break; } } }; const auto findInlineFormulaProbe = [&]( const MarkdownArticleContent &prepared, const QString &segmentText) { auto result = SegmentTextProbe(); forEachPreparedTextSegment(prepared, [&]( int segmentIndex, const TextWithEntities &text) { if (!segmentText.isEmpty() && !text.text.contains(segmentText)) { return false; } for (const auto &match : CollectInlineTextObjectMatches(text)) { if (match.object.kind == InlineTextObjectKind::Formula) { result = { .segmentIndex = segmentIndex, .offset = match.entity.offset(), .length = match.entity.length(), .segment = text, }; return true; } } return false; }); return result; }; const auto findFirstInlineFormulaProbe = [&]( const MarkdownArticleContent &prepared) { return findInlineFormulaProbe(prepared, QString()); }; const auto findTextProbe = [&]( const MarkdownArticleContent &prepared, const QString &segmentText, const QString &text, int requiredSegmentIndex = -1) { auto result = SegmentTextProbe(); forEachPreparedTextSegment(prepared, [&]( int segmentIndex, const TextWithEntities &segment) { if ((requiredSegmentIndex >= 0) && (segmentIndex != requiredSegmentIndex)) { return false; } if (!segmentText.isEmpty() && !segment.text.contains(segmentText)) { return false; } const auto offset = int(segment.text.indexOf(text)); if (offset < 0) { return false; } result = { .segmentIndex = segmentIndex, .offset = offset, .length = int(text.size()), .segment = segment, }; return true; }); return result; }; const auto findTextOffset = [&]( const MarkdownArticleContent &prepared, const QString &text) { return findTextProbe(prepared, QString(), text).offset; }; const auto firstMeasuredInlineFormulaWidth = []( const MarkdownArticleContent &prepared) { for (const auto &slot : prepared.formulas) { if (slot.present && (slot.kind == MathKind::Inline) && slot.measured.success) { return slot.measured.logicalSize.width(); } } return 0; }; const auto logicalRectToImageRect = [](QRect rect, int devicePixelRatio) { return QRect( rect.x() * devicePixelRatio, rect.y() * devicePixelRatio, rect.width() * devicePixelRatio, rect.height() * devicePixelRatio); }; const auto withinOneLogicalPixel = [](double left, double right) { const auto delta = left - right; return (delta <= 1.) && (delta >= -1.); }; const auto sameVerticalBounds = []( const QRect &left, const QRect &right) { return (left.top() == right.top()) && (left.bottom() == right.bottom()); }; const auto markerSearchStripFromLineBounds = [](QRect lineBounds) { lineBounds = lineBounds.adjusted(0, -1, 0, 1); return QRect( 0, lineBounds.y(), std::max(lineBounds.left(), 1), std::max(lineBounds.height(), 1)); }; const auto buildSegmentLeaf = [&]( const MarkdownArticleContent &prepared, const SegmentTextProbe &probe, const std::shared_ptr &renderer, int width) { auto leaf = Ui::Text::String(); auto cache = CreateInlineFormulaObjectCache(renderer); SetTextLeaf( &leaf, st::defaultMarkdown.body, probe.segment, &prepared.formulas, cache.get(), prepared.mediaRuntime, width); return leaf; }; const auto resolveSegmentBaseline = [&]( const QString &label, const MarkdownArticleContent &prepared, const SegmentTextProbe &probe, const std::shared_ptr &renderer, const QRect &lineBounds, int width) -> std::optional { const auto leaf = buildSegmentLeaf(prepared, probe, renderer, width); const auto lines = leaf.countLinesGeometry(width, true); Check( !lines.empty(), label + FromLatin1(" control line geometry"), ok); return !lines.empty() ? std::make_optional(lineBounds.top() + lines.front().baseline) : std::nullopt; }; struct ArticleLayoutProbe { int width = 0; int height = 0; }; const auto resolveArticleWidth = [&]( const QString &label, const MarkdownArticleContent &prepared, const std::shared_ptr &renderer, const SegmentTextProbe &formulaProbe, const SegmentTextProbe &controlProbe) { auto result = ArticleLayoutProbe(); for (auto width = 320; width <= 1280; width += 32) { auto height = 0; auto article = BuildArticleForTest(prepared, renderer, width, &height); const auto formulaBounds = SymbolHitBounds( article.get(), width, height, formulaProbe.offset, formulaProbe.segmentIndex); const auto controlBounds = SymbolRangeHitBounds( article.get(), width, height, controlProbe.offset, controlProbe.length, controlProbe.segmentIndex); if (!formulaBounds || !controlBounds || !sameVerticalBounds(*formulaBounds, *controlBounds)) { continue; } result = { .width = width, .height = height, }; break; } Check( result.width > 0, label + FromLatin1(" derived article width"), ok); return result; }; const auto checkListMarkerBaselineRegression = [&]( const QString &label, const MarkdownArticleContent &prepared, const std::shared_ptr &renderer, const SegmentTextProbe &referenceControlProbe, const SegmentTextProbe &formulaProbe, const SegmentTextProbe &controlProbe) { Check( referenceControlProbe.valid(), label + FromLatin1(" plain-text reference probe"), ok); Check( formulaProbe.valid(), label + FromLatin1(" inline formula probe"), ok); Check( controlProbe.valid(), label + FromLatin1(" plain-text control probe"), ok); if (!referenceControlProbe.valid() || !formulaProbe.valid() || !controlProbe.valid()) { return; } Check( formulaProbe.segmentIndex == controlProbe.segmentIndex, label + FromLatin1(" formula and control share a segment"), ok); Check( referenceControlProbe.segmentIndex != controlProbe.segmentIndex, label + FromLatin1(" reference and formula use distinct segments"), ok); if ((formulaProbe.segmentIndex != controlProbe.segmentIndex) || (referenceControlProbe.segmentIndex == controlProbe.segmentIndex)) { return; } const auto layout = resolveArticleWidth( label, prepared, renderer, formulaProbe, controlProbe); if (layout.width <= 0) { return; } auto articleHeight = 0; auto article = BuildArticleForTest( prepared, renderer, layout.width, &articleHeight); const auto image = PaintArticleForTest( article.get(), layout.width, articleHeight); Check( HasPaintedPixels(image), label + FromLatin1(" paint produced pixels"), ok); const auto referenceControlBounds = SymbolRangeHitBounds( article.get(), layout.width, articleHeight, referenceControlProbe.offset, referenceControlProbe.length, referenceControlProbe.segmentIndex); const auto formulaBounds = SymbolHitBounds( article.get(), layout.width, articleHeight, formulaProbe.offset, formulaProbe.segmentIndex); const auto controlBounds = SymbolRangeHitBounds( article.get(), layout.width, articleHeight, controlProbe.offset, controlProbe.length, controlProbe.segmentIndex); Check( referenceControlBounds.has_value(), label + FromLatin1(" reference control hit bounds"), ok); Check( formulaBounds.has_value(), label + FromLatin1(" inline formula hit bounds"), ok); Check( controlBounds.has_value(), label + FromLatin1(" plain-text control hit bounds"), ok); if (!referenceControlBounds || !formulaBounds || !controlBounds) { return; } Check( sameVerticalBounds(*formulaBounds, *controlBounds), label + FromLatin1(" formula stays on first rendered line"), ok); const auto referenceBaseline = resolveSegmentBaseline( label + FromLatin1(" reference"), prepared, referenceControlProbe, renderer, *referenceControlBounds, layout.width); const auto controlBaseline = resolveSegmentBaseline( label, prepared, controlProbe, renderer, *controlBounds, layout.width); Check( referenceBaseline.has_value(), label + FromLatin1(" reference baseline"), ok); Check( controlBaseline.has_value(), label + FromLatin1(" control baseline"), ok); if (!referenceBaseline || !controlBaseline) { return; } const auto referenceMarkerPainted = PaintedBoundsInRect( image, markerSearchStripFromLineBounds(*referenceControlBounds)); const auto markerPainted = PaintedBoundsInRect( image, markerSearchStripFromLineBounds(*controlBounds)); Check( referenceMarkerPainted.has_value(), label + FromLatin1(" reference gutter marker painted bounds"), ok); Check( markerPainted.has_value(), label + FromLatin1(" gutter marker painted bounds"), ok); if (!referenceMarkerPainted || !markerPainted) { return; } Check( referenceMarkerPainted->right() < referenceControlBounds->left(), label + FromLatin1(" reference gutter marker stays left of text"), ok); Check( markerPainted->right() < controlBounds->left(), label + FromLatin1(" gutter marker stays left of text"), ok); Check( withinOneLogicalPixel( double(markerPainted->center().y() - referenceMarkerPainted->center().y()), double(*controlBaseline - *referenceBaseline)), label + FromLatin1(" gutter marker follows baseline shift"), ok); }; const auto wideInlineLabel = FromLatin1("generated-inline-wide-raster.md"); const auto wideInlineParsed = ParseMarkdownForIv( QByteArray("$") + wideFormulaTex + QByteArray("$\n"), ParseOptions{ wideInlineLabel }); Check( wideInlineParsed.ok, wideInlineLabel + FromLatin1(" parse failed: ") + wideInlineParsed.error, ok); if (wideInlineParsed.ok) { const auto articleWidth = 180; auto renderer = std::make_shared(); auto prepared = PrepareParsedDocumentForTest( wideInlineParsed.document, wideInlineLabel, renderer); Check( !prepared.failure.failed(), wideInlineLabel + FromLatin1(" prepare failure: ") + PrepareFailureReason(prepared.failure), ok); if (!prepared.failure.failed()) { const auto formulaWidth = firstMeasuredInlineFormulaWidth(prepared); const auto formulaProbe = findFirstInlineFormulaProbe(prepared); Check( formulaWidth > articleWidth, wideInlineLabel + FromLatin1(" formula is wider than article"), ok); Check( formulaProbe.valid(), wideInlineLabel + FromLatin1(" inline formula probe"), ok); auto articleHeight = 0; auto article = BuildArticleForTest( std::move(prepared), renderer, articleWidth, &articleHeight); const auto image = PaintArticleForTest( article.get(), articleWidth, articleHeight); Check( HasPaintedPixels(image), wideInlineLabel + FromLatin1(" paint produced pixels"), ok); const auto bounds = SymbolHitBounds( article.get(), articleWidth, articleHeight, formulaProbe.offset, formulaProbe.segmentIndex); Check( bounds.has_value(), wideInlineLabel + FromLatin1(" inline formula hit bounds"), ok); Check( bounds && PaintedBoundsInRect(image, *bounds).has_value(), wideInlineLabel + FromLatin1(" inline formula paints at line start"), ok); } } const auto tableLabel = FromLatin1("generated-table-inline-wide-raster.md"); const auto tableParsed = ParseMarkdownForIv( QByteArray("| Header | Notes | Tail |\n" "| --- | --- | --- |\n" "| $") + wideFormulaTex + QByteArray("$ | cell | tail |\n"), ParseOptions{ tableLabel }); Check( tableParsed.ok, tableLabel + FromLatin1(" parse failed: ") + tableParsed.error, ok); if (tableParsed.ok) { const auto articleWidth = 240; auto renderer = std::make_shared(); auto prepared = PrepareParsedDocumentForTest( tableParsed.document, tableLabel, renderer); Check( !prepared.failure.failed(), tableLabel + FromLatin1(" prepare failure: ") + PrepareFailureReason(prepared.failure), ok); if (!prepared.failure.failed()) { const auto formulaWidth = firstMeasuredInlineFormulaWidth(prepared); const auto formulaProbe = findFirstInlineFormulaProbe(prepared); Check( formulaWidth > (articleWidth / 3), tableLabel + FromLatin1(" formula is wider than cell budget"), ok); Check( formulaProbe.valid(), tableLabel + FromLatin1(" table formula probe"), ok); auto articleHeight = 0; auto article = BuildArticleForTest( std::move(prepared), renderer, articleWidth, &articleHeight); const auto image = PaintArticleForTest( article.get(), articleWidth, articleHeight); Check( HasPaintedPixels(image), tableLabel + FromLatin1(" paint produced pixels"), ok); const auto bounds = SymbolHitBounds( article.get(), articleWidth, articleHeight, formulaProbe.offset, formulaProbe.segmentIndex); Check( bounds.has_value(), tableLabel + FromLatin1(" table formula hit bounds"), ok); Check( bounds && PaintedBoundsInRect(image, *bounds).has_value(), tableLabel + FromLatin1(" table formula paints in narrow cell"), ok); } } const auto baselineLabel = FromLatin1("generated-inline-baseline-dpr.md"); const auto baselineParsed = ParseMarkdownForIv( QByteArray( "Multiple inline: We have $a = 1$, and plain text a = 1" " on the same line.\n"), ParseOptions{ baselineLabel }); Check( baselineParsed.ok, baselineLabel + FromLatin1(" parse failed: ") + baselineParsed.error, ok); if (baselineParsed.ok) { const auto articleWidth = 480; auto renderer = std::make_shared(); auto prepared = PrepareParsedDocumentForTest( baselineParsed.document, baselineLabel, renderer); Check( !prepared.failure.failed(), baselineLabel + FromLatin1(" prepare failure: ") + PrepareFailureReason(prepared.failure), ok); if (!prepared.failure.failed()) { const auto formulaProbe = findFirstInlineFormulaProbe(prepared); const auto controlText = FromLatin1("a = 1"); const auto controlOffset = findTextOffset(prepared, controlText); Check( formulaProbe.valid(), baselineLabel + FromLatin1(" inline formula probe"), ok); Check( controlOffset >= 0, baselineLabel + FromLatin1(" plain-text control offset"), ok); auto articleHeight = 0; auto article = BuildArticleForTest( std::move(prepared), renderer, articleWidth, &articleHeight); const auto dpr1Image = PaintArticleForTest( article.get(), articleWidth, articleHeight, 1); const auto dpr2Image = PaintArticleForTest( article.get(), articleWidth, articleHeight, 2); Check( HasPaintedPixels(dpr1Image), baselineLabel + FromLatin1(" DPR1 paint produced pixels"), ok); Check( HasPaintedPixels(dpr2Image), baselineLabel + FromLatin1(" DPR2 paint produced pixels"), ok); const auto bounds = SymbolHitBounds( article.get(), articleWidth, articleHeight, formulaProbe.offset, formulaProbe.segmentIndex); Check( bounds.has_value(), baselineLabel + FromLatin1(" inline formula hit bounds"), ok); const auto controlBounds = SymbolRangeHitBounds( article.get(), articleWidth, articleHeight, controlOffset, controlText.size()); Check( controlBounds.has_value(), baselineLabel + FromLatin1(" plain-text control hit bounds"), ok); Check( article->segmentIsText(0), baselineLabel + FromLatin1(" first segment is text"), ok); if (bounds && controlBounds) { const auto dpr1Painted = PaintedBoundsInRect(dpr1Image, *bounds); const auto dpr1ControlPainted = PaintedBoundsInRect( dpr1Image, *controlBounds); const auto dpr2Painted = PaintedBoundsInRect( dpr2Image, logicalRectToImageRect(*bounds, 2)); const auto dpr2ControlPainted = PaintedBoundsInRect( dpr2Image, logicalRectToImageRect(*controlBounds, 2)); Check( dpr1Painted.has_value(), baselineLabel + FromLatin1(" DPR1 painted bounds"), ok); Check( dpr1ControlPainted.has_value(), baselineLabel + FromLatin1(" DPR1 plain-text control bounds"), ok); Check( dpr2Painted.has_value(), baselineLabel + FromLatin1(" DPR2 painted bounds"), ok); Check( dpr2ControlPainted.has_value(), baselineLabel + FromLatin1(" DPR2 plain-text control bounds"), ok); if (dpr1Painted && dpr1ControlPainted && dpr2Painted && dpr2ControlPainted) { const auto formulaExtendsAboveControl = dpr1Painted->top() < dpr1ControlPainted->top(); const auto formulaExtendsBelowControl = dpr1Painted->bottom() > dpr1ControlPainted->bottom(); Check( withinOneLogicalPixel( double(dpr1Painted->bottom()), double(dpr1ControlPainted->bottom())), baselineLabel + FromLatin1( " DPR1 formula bottom matches plain-text control"), ok); Check( withinOneLogicalPixel( double(dpr2Painted->bottom()) / 2., double(dpr2ControlPainted->bottom()) / 2.), baselineLabel + FromLatin1( " DPR2 formula bottom matches plain-text control"), ok); Check( withinOneLogicalPixel( double(dpr1Painted->top()), double(dpr2Painted->top()) / 2.), baselineLabel + FromLatin1(" DPR top alignment"), ok); Check( withinOneLogicalPixel( double(dpr1Painted->bottom()), double(dpr2Painted->bottom()) / 2.), baselineLabel + FromLatin1(" DPR bottom alignment"), ok); if (article->segmentIsText(0)) { const auto selectedImage = PaintArticleForTest( article.get(), articleWidth, articleHeight, 1, MarkdownArticleSelection{ .from = { .segment = 0, .offset = 0 }, .to = { .segment = 0, .offset = article->segmentLength(0), }, }); const auto selectionFormulaBounds = ChangedBoundsInRect( dpr1Image, selectedImage, QRect(bounds->x(), 0, bounds->width(), articleHeight)); const auto selectionControlBounds = ChangedBoundsInRect( dpr1Image, selectedImage, QRect( controlBounds->x(), 0, controlBounds->width(), articleHeight)); Check( selectionFormulaBounds.has_value(), baselineLabel + FromLatin1( " formula selection changed bounds"), ok); Check( selectionControlBounds.has_value(), baselineLabel + FromLatin1( " plain-text control selection changed bounds"), ok); if (selectionFormulaBounds && selectionControlBounds) { Check( selectionFormulaBounds->top() <= selectionControlBounds->top(), baselineLabel + FromLatin1( " formula selection reaches control top"), ok); Check( selectionFormulaBounds->bottom() >= selectionControlBounds->bottom(), baselineLabel + FromLatin1( " formula selection reaches control bottom"), ok); if (formulaExtendsAboveControl) { Check( selectionFormulaBounds->top() < selectionControlBounds->top(), baselineLabel + FromLatin1( " formula selection keeps extra top"), ok); } if (formulaExtendsBelowControl) { Check( selectionFormulaBounds->bottom() > selectionControlBounds->bottom(), baselineLabel + FromLatin1( " formula selection keeps extra bottom"), ok); } } } } } } const auto orderedListLabel = FromLatin1( "generated-ordered-list-marker-baseline.md"); const auto orderedParsed = ParseMarkdownForIv( QByteArray( "3. Third with fraction: p / q\n" "\n" "3. Third with fraction: $\\frac{p}{q}$\n"), ParseOptions{ orderedListLabel }); Check( orderedParsed.ok, orderedListLabel + FromLatin1(" parse failed: ") + orderedParsed.error, ok); if (orderedParsed.ok) { auto orderedRenderer = std::make_shared(); auto orderedPrepared = PrepareParsedDocumentForTest( orderedParsed.document, orderedListLabel, orderedRenderer); Check( !orderedPrepared.failure.failed(), orderedListLabel + FromLatin1(" prepare failure: ") + PrepareFailureReason(orderedPrepared.failure), ok); if (!orderedPrepared.failure.failed()) { const auto controlText = FromLatin1("Third with fraction:"); const auto formulaProbe = findInlineFormulaProbe( orderedPrepared, controlText); const auto referenceControlProbe = findTextProbe( orderedPrepared, controlText, controlText); const auto controlProbe = findTextProbe( orderedPrepared, controlText, controlText, formulaProbe.segmentIndex); checkListMarkerBaselineRegression( orderedListLabel, orderedPrepared, orderedRenderer, referenceControlProbe, formulaProbe, controlProbe); } } const auto bulletListLabel = FromLatin1( "generated-bullet-list-marker-baseline.md"); const auto bulletParsed = ParseMarkdownForIv( QByteArray( "- Bullet with fraction: p / q\n" "\n" "- Bullet with fraction: $\\frac{p}{q}$\n"), ParseOptions{ bulletListLabel }); Check( bulletParsed.ok, bulletListLabel + FromLatin1(" parse failed: ") + bulletParsed.error, ok); if (bulletParsed.ok) { auto bulletRenderer = std::make_shared(); auto bulletPrepared = PrepareParsedDocumentForTest( bulletParsed.document, bulletListLabel, bulletRenderer); Check( !bulletPrepared.failure.failed(), bulletListLabel + FromLatin1(" prepare failure: ") + PrepareFailureReason(bulletPrepared.failure), ok); if (!bulletPrepared.failure.failed()) { const auto controlText = FromLatin1("Bullet with fraction:"); const auto formulaProbe = findInlineFormulaProbe( bulletPrepared, controlText); const auto referenceControlProbe = findTextProbe( bulletPrepared, controlText, controlText); const auto controlProbe = findTextProbe( bulletPrepared, controlText, controlText, formulaProbe.segmentIndex); checkListMarkerBaselineRegression( bulletListLabel, bulletPrepared, bulletRenderer, referenceControlProbe, formulaProbe, controlProbe); } } } } void CheckArticleRenderSmoke( const PreparedFixture &markdownFixture, const PreparedFixture &latexFixture, bool *ok) { Check( MicrotexBackendLinked(), FromLatin1("microtex backend should be linked"), ok); auto marginRenderer = MathRenderer(); const auto dimensions = CaptureMarkdownPrepareDimensions(); const auto checkFormulaRasterMargins = [&]( const QString &label, const QString &tex, MathKind kind, int textSize) { const auto rendered = marginRenderer.renderFormula({ .trimmedTex = tex, .kind = kind, .textSize = textSize, .renderWidthCap = dimensions.displayMathMaxRenderWidth, .renderHeightCap = dimensions.displayMathMaxRenderHeight, .devicePixelRatio = 2, }); Check( rendered.success, label + FromLatin1(" formula raster success"), ok); if (rendered.success) { Check( !PaintTouchesBottomOrRightImageEdge(rendered.image), label + FromLatin1(" formula raster margin"), ok); } }; checkFormulaRasterMargins( FromLatin1("inline-x"), FromLatin1("x"), MathKind::Inline, dimensions.bodyTextSize); checkFormulaRasterMargins( FromLatin1("inline-y"), FromLatin1("y"), MathKind::Inline, dimensions.bodyTextSize); checkFormulaRasterMargins( FromLatin1("inline-alpha"), FromLatin1("\\alpha"), MathKind::Inline, dimensions.bodyTextSize); checkFormulaRasterMargins( FromLatin1("inline-beta"), FromLatin1("\\beta"), MathKind::Inline, dimensions.bodyTextSize); checkFormulaRasterMargins( FromLatin1("display-quadratic"), FromLatin1("x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}"), MathKind::Display, dimensions.displayMathTextSize); auto renderer = std::make_shared(); auto firstMarkdown = PrepareParsedDocumentForTest( markdownFixture.parsed, markdownFixture.path, renderer); auto firstLatex = PrepareParsedDocumentForTest( latexFixture.parsed, latexFixture.path, renderer); Check( !firstMarkdown.failure.failed() && !firstLatex.failure.failed(), FromLatin1("article prepare smoke first pass failed"), ok); Check( renderer->cacheUsageBytes() == 0, FromLatin1("article prepare smoke keeps renderer cache empty"), ok); const auto prepareCounters = renderer->debugCounters(); Check( prepareCounters.hits == 0 && prepareCounters.misses == 0 && prepareCounters.rendered == 0, FromLatin1("article prepare smoke has no raster work"), ok); auto prepareSmokeLine = FromLatin1("prepare-lazy-smoke"); prepareSmokeLine.append(FromLatin1(" measured_slots=")); prepareSmokeLine.append(QString::number( CountMeasuredFormulaSlots(firstMarkdown) + CountMeasuredFormulaSlots(firstLatex))); prepareSmokeLine.append(FromLatin1(" hits=")); prepareSmokeLine.append(QString::number(prepareCounters.hits)); prepareSmokeLine.append(FromLatin1(" misses=")); prepareSmokeLine.append(QString::number(prepareCounters.misses)); prepareSmokeLine.append(FromLatin1(" rendered=")); prepareSmokeLine.append(QString::number(prepareCounters.rendered)); PrintLine(prepareSmokeLine); const auto articleWidth = 640; const auto expectedArticleFormulaHits = CountMeasuredFormulaSlots( firstMarkdown); auto firstArticleHeight = 0; auto firstArticle = BuildArticleForTest( std::move(firstMarkdown), renderer, articleWidth, &firstArticleHeight); const auto firstImage = PaintArticleForTest( firstArticle.get(), articleWidth, firstArticleHeight); Check( HasPaintedPixels(firstImage), FromLatin1("article first paint produced pixels"), ok); const auto firstPaintCounters = renderer->debugCounters(); Check( firstPaintCounters.misses >= expectedArticleFormulaHits, FromLatin1("article first paint lazy raster misses"), ok); Check( firstPaintCounters.rendered >= expectedArticleFormulaHits, FromLatin1("article first paint lazy raster rendered"), ok); Check( renderer->cacheUsageBytes() > 0, FromLatin1("article first paint populated renderer cache"), ok); const auto secondImage = PaintArticleForTest( firstArticle.get(), articleWidth, firstArticleHeight); Check( HasPaintedPixels(secondImage), FromLatin1("article second paint produced pixels"), ok); const auto secondPaintCounters = renderer->debugCounters(); Check( secondPaintCounters.hits == firstPaintCounters.hits && secondPaintCounters.misses == firstPaintCounters.misses && secondPaintCounters.rendered == firstPaintCounters.rendered, FromLatin1("article same-instance raster cache reuse"), ok); auto articleSmokeLine = FromLatin1("article-lazy-smoke"); articleSmokeLine.append(FromLatin1(" first_hits=")); articleSmokeLine.append(QString::number(firstPaintCounters.hits)); articleSmokeLine.append(FromLatin1(" first_misses=")); articleSmokeLine.append(QString::number(firstPaintCounters.misses)); articleSmokeLine.append(FromLatin1(" first_rendered=")); articleSmokeLine.append(QString::number(firstPaintCounters.rendered)); articleSmokeLine.append(FromLatin1(" second_hits=")); articleSmokeLine.append(QString::number(secondPaintCounters.hits)); articleSmokeLine.append(FromLatin1(" second_misses=")); articleSmokeLine.append(QString::number(secondPaintCounters.misses)); PrintLine(articleSmokeLine); renderer->resetDebugCounters(); auto secondMarkdown = PrepareParsedDocumentForTest( markdownFixture.parsed, markdownFixture.path, renderer); Check( !secondMarkdown.failure.failed(), FromLatin1("article renderer cache smoke second prepare failed"), ok); auto secondArticleHeight = 0; auto secondArticle = BuildArticleForTest( std::move(secondMarkdown), renderer, articleWidth, &secondArticleHeight); const auto cachedImage = PaintArticleForTest( secondArticle.get(), articleWidth, secondArticleHeight); Check( HasPaintedPixels(cachedImage), FromLatin1("article renderer cache paint produced pixels"), ok); const auto rendererReuseCounters = renderer->debugCounters(); Check( rendererReuseCounters.hits >= expectedArticleFormulaHits, FromLatin1("article renderer cache reuse hits"), ok); Check( rendererReuseCounters.misses == 0, FromLatin1("article renderer cache reuse misses stay zero"), ok); Check( rendererReuseCounters.rendered == 0, FromLatin1("article renderer cache reuse rendered stays zero"), ok); auto rendererSmokeLine = FromLatin1("renderer-cache-smoke"); rendererSmokeLine.append(FromLatin1(" hits=")); rendererSmokeLine.append(QString::number(rendererReuseCounters.hits)); rendererSmokeLine.append(FromLatin1(" misses=")); rendererSmokeLine.append(QString::number(rendererReuseCounters.misses)); rendererSmokeLine.append(FromLatin1(" rendered=")); rendererSmokeLine.append(QString::number(rendererReuseCounters.rendered)); PrintLine(rendererSmokeLine); const auto sharedMarkdown = std::make_shared( markdownFixture.parsed); auto measurementFirst = PrepareParsedDocumentForTest( sharedMarkdown, markdownFixture.path, std::make_shared()); Check( !measurementFirst.failure.failed(), FromLatin1("document measurement cache smoke first pass failed"), ok); auto measurementSecond = PrepareParsedDocumentForTest( sharedMarkdown, markdownFixture.path, std::make_shared()); Check( !measurementSecond.failure.failed(), FromLatin1("document measurement cache smoke second pass failed"), ok); const auto firstMeasurements = MeasuredFormulaPointers(measurementFirst); const auto secondMeasurements = MeasuredFormulaPointers(measurementSecond); Check( !firstMeasurements.empty() && (firstMeasurements.size() == secondMeasurements.size()), FromLatin1("document measurement cache smoke pointer shape"), ok); for (auto i = 0, count = int(firstMeasurements.size()); i != count; ++i) { Check( firstMeasurements[i] == secondMeasurements[i], FromLatin1("document measurement cache smoke pointer reuse"), ok); } auto documentSmokeLine = FromLatin1("document-cache-smoke"); documentSmokeLine.append(FromLatin1(" slots=")); documentSmokeLine.append(QString::number(firstMeasurements.size())); documentSmokeLine.append(FromLatin1(" reused=")); documentSmokeLine.append(YesNo(firstMeasurements == secondMeasurements)); PrintLine(documentSmokeLine); const auto failureLabel = FromLatin1("generated-formula-cap.md"); const auto failureParsed = ParseMarkdownForIv( QByteArray("$$\nE = mc^2\n$$\n"), ParseOptions{ failureLabel }); Check( failureParsed.ok, failureLabel + FromLatin1(" parse failed: ") + failureParsed.error, ok); if (!failureParsed.ok) { return; } auto failureDimensions = CaptureMarkdownPrepareDimensions(); failureDimensions.displayMathMaxRenderWidth = 1; auto failureRenderer = std::make_shared(); auto failurePrepared = PrepareParsedDocumentForTest( failureParsed.document, failureLabel, failureRenderer, std::move(failureDimensions)); Check( !failurePrepared.failure.failed(), failureLabel + FromLatin1(" terminal prepare failure"), ok); Check( failurePrepared.debug.formulaWarningCount > 0, failureLabel + FromLatin1(" formula warning count"), ok); auto failedFormulaFound = false; for (const auto &slot : failurePrepared.formulas) { if (!slot.present) { continue; } if (!slot.measured.success && (slot.measured.tooLarge || slot.measured.overflow) && !slot.measured.fallbackText.isEmpty()) { failedFormulaFound = true; } } Check( failedFormulaFound, failureLabel + FromLatin1(" formula cap fallback result"), ok); auto fallbackArticleHeight = 0; auto fallbackArticle = BuildArticleForTest( std::move(failurePrepared), failureRenderer, articleWidth, &fallbackArticleHeight); const auto fallbackImage = PaintArticleForTest( fallbackArticle.get(), articleWidth, fallbackArticleHeight); Check( HasPaintedPixels(fallbackImage), failureLabel + FromLatin1(" fallback paint produced pixels"), ok); const auto fallbackCounters = failureRenderer->debugCounters(); Check( fallbackCounters.hits == 0 && fallbackCounters.misses == 0 && fallbackCounters.rendered == 0, failureLabel + FromLatin1(" fallback paint avoided raster renderer"), ok); const auto inlineFailureLabel = FromLatin1("generated-inline-formula-cap.md"); const auto inlineFailureParsed = ParseMarkdownForIv( QByteArray("Inline fallback $\\frac{a}{b}$ text.\n"), ParseOptions{ inlineFailureLabel }); Check( inlineFailureParsed.ok, inlineFailureLabel + FromLatin1(" parse failed: ") + inlineFailureParsed.error, ok); if (!inlineFailureParsed.ok) { return; } auto inlineFailureDimensions = CaptureMarkdownPrepareDimensions(); inlineFailureDimensions.displayMathMaxRenderWidth = 1; auto inlineFailureRenderer = std::make_shared(); auto inlineFailurePrepared = PrepareParsedDocumentForTest( inlineFailureParsed.document, inlineFailureLabel, inlineFailureRenderer, std::move(inlineFailureDimensions)); Check( !inlineFailurePrepared.failure.failed(), inlineFailureLabel + FromLatin1(" terminal prepare failure"), ok); auto inlineMeasuredFallbackText = QString(); for (const auto &slot : inlineFailurePrepared.formulas) { if (!slot.present || (slot.kind != MathKind::Inline)) { continue; } if (!slot.measured.success && (slot.measured.tooLarge || slot.measured.overflow) && (slot.measured.logicalSize.width() > 0) && !slot.measured.fallbackText.isEmpty()) { inlineMeasuredFallbackText = slot.measured.fallbackText; break; } } auto inlineDisplayedFallbackText = QString(); auto inlineFailureObjectOffset = -1; ForEachPreparedBlock( inlineFailurePrepared.blocks.blocks, [&](const PreparedBlock &block) { if (!inlineDisplayedFallbackText.isEmpty()) { return; } for (const auto &match : CollectInlineTextObjectMatches(block.text)) { if (match.object.kind != InlineTextObjectKind::Formula) { continue; } const auto formula = std::get_if( &match.object.data); if (!formula || formula->copySource.isEmpty()) { continue; } inlineDisplayedFallbackText = formula->copySource; inlineFailureObjectOffset = match.entity.offset(); break; } }); Check( !inlineMeasuredFallbackText.isEmpty(), inlineFailureLabel + FromLatin1(" inline fallback result"), ok); Check( !inlineDisplayedFallbackText.isEmpty(), inlineFailureLabel + FromLatin1(" inline displayed fallback text"), ok); Check( inlineFailureObjectOffset >= 0, inlineFailureLabel + FromLatin1(" inline object offset"), ok); Check( inlineDisplayedFallbackText != inlineMeasuredFallbackText, inlineFailureLabel + FromLatin1( " displayed fallback differs from trimmed fallback"), ok); if (!inlineFailurePrepared.failure.failed() && !inlineDisplayedFallbackText.isEmpty() && (inlineFailureObjectOffset >= 0)) { auto inlineFailureArticleHeight = 0; auto inlineFailureArticle = BuildArticleForTest( std::move(inlineFailurePrepared), inlineFailureRenderer, articleWidth, &inlineFailureArticleHeight); const auto inlineFailureImage = PaintArticleForTest( inlineFailureArticle.get(), articleWidth, inlineFailureArticleHeight); Check( HasPaintedPixels(inlineFailureImage), inlineFailureLabel + FromLatin1(" fallback paint produced pixels"), ok); const auto inlineFailureCounters = inlineFailureRenderer->debugCounters(); Check( inlineFailureCounters.hits == 0 && inlineFailureCounters.misses == 0 && inlineFailureCounters.rendered == 0, inlineFailureLabel + FromLatin1(" fallback paint avoided raster renderer"), ok); const auto inlineFailureHitBounds = SymbolHitBounds( inlineFailureArticle.get(), articleWidth, inlineFailureArticleHeight, inlineFailureObjectOffset); Check( inlineFailureHitBounds.has_value(), inlineFailureLabel + FromLatin1(" inline hit bounds"), ok); const auto inlineFailurePaintStrip = inlineFailureHitBounds ? QRect( inlineFailureHitBounds->x(), 0, inlineFailureHitBounds->width(), inlineFailureImage.height()) : QRect(); const auto inlineFailurePaintedBounds = inlineFailureHitBounds ? PaintedBoundsInRect(inlineFailureImage, inlineFailurePaintStrip) : std::nullopt; Check( inlineFailurePaintedBounds.has_value(), inlineFailureLabel + FromLatin1(" fallback paint strip bounds"), ok); if (inlineFailureHitBounds && inlineFailurePaintedBounds) { Check( inlineFailurePaintedBounds->top() >= inlineFailureHitBounds->top() && inlineFailurePaintedBounds->bottom() <= inlineFailureHitBounds->bottom(), inlineFailureLabel + FromLatin1(" fallback paint stays within line bounds"), ok); } const auto inlinePlainLabel = FromLatin1( "generated-inline-formula-cap-plain.md"); auto inlinePlainText = inlineDisplayedFallbackText; inlinePlainText.replace(u"$"_q, u"\\$"_q); const auto inlinePlainSource = QByteArray("Inline fallback ") + inlinePlainText.toUtf8() + QByteArray(" text.\n"); const auto inlinePlainParsed = ParseMarkdownForIv( inlinePlainSource, ParseOptions{ inlinePlainLabel }); Check( inlinePlainParsed.ok, inlinePlainLabel + FromLatin1(" parse failed: ") + inlinePlainParsed.error, ok); if (inlinePlainParsed.ok) { auto inlinePlainRenderer = std::make_shared(); auto inlinePlainPrepared = PrepareParsedDocumentForTest( inlinePlainParsed.document, inlinePlainLabel, inlinePlainRenderer); Check( !inlinePlainPrepared.failure.failed(), inlinePlainLabel + FromLatin1(" prepare failure: ") + PrepareFailureReason(inlinePlainPrepared.failure), ok); if (!inlinePlainPrepared.failure.failed()) { auto inlinePlainArticleHeight = 0; auto inlinePlainArticle = BuildArticleForTest( std::move(inlinePlainPrepared), inlinePlainRenderer, articleWidth, &inlinePlainArticleHeight); const auto inlinePlainImage = PaintArticleForTest( inlinePlainArticle.get(), articleWidth, inlinePlainArticleHeight); Check( inlineFailureArticle->maxWidth() == inlinePlainArticle->maxWidth(), inlineFailureLabel + FromLatin1(" fallback max width matches plain text"), ok); Check( inlineFailureArticleHeight == inlinePlainArticleHeight, inlineFailureLabel + FromLatin1(" fallback height matches plain text"), ok); const auto inlinePlainPaintedBounds = inlineFailureHitBounds ? PaintedBoundsInRect(inlinePlainImage, inlineFailurePaintStrip) : std::nullopt; Check( inlinePlainPaintedBounds.has_value(), inlinePlainLabel + FromLatin1(" fallback paint strip bounds"), ok); if (inlineFailurePaintedBounds && inlinePlainPaintedBounds) { Check( inlineFailurePaintedBounds->top() == inlinePlainPaintedBounds->top() && inlineFailurePaintedBounds->bottom() == inlinePlainPaintedBounds->bottom(), inlineFailureLabel + FromLatin1( " fallback paint vertical bounds match plain text"), ok); } } } } const auto &prepareLimits = PrepareLimitsForIv(); auto blockLimitSource = QByteArray(); for (auto i = 0; i != (prepareLimits.maxPreparedBlocks + 1); ++i) { blockLimitSource.append("Paragraph "); blockLimitSource.append(QByteArray::number(i)); blockLimitSource.append("\n\n"); } const auto blockLimitLabel = FromLatin1("generated-prepare-block-limit.md"); const auto blockLimitParsed = ParseMarkdownForIv( blockLimitSource, ParseOptions{ blockLimitLabel }); Check( blockLimitParsed.ok, blockLimitLabel + FromLatin1(" parse failed: ") + blockLimitParsed.error, ok); if (blockLimitParsed.ok) { const auto blockLimitPrepared = PrepareParsedDocumentForTest( blockLimitParsed.document, blockLimitLabel, std::make_shared()); Check( blockLimitPrepared.failure.failed(), blockLimitLabel + FromLatin1( " missing real terminal prepare failure"), ok); Check( blockLimitPrepared.failure.terminal == PrepareTerminalFailure::DocumentTooLarge, blockLimitLabel + FromLatin1( " real terminal prepare failure kind"), ok); Check( PrepareFailureReason(blockLimitPrepared.failure) == FromLatin1("prepared-block-limit"), blockLimitLabel + FromLatin1( " real terminal prepare failure reason"), ok); Check( blockLimitPrepared.blocks.blocks.empty(), blockLimitLabel + FromLatin1( " real terminal prepare clears blocks"), ok); } } void CheckAnchorScrollAlignmentCoverage(bool *ok) { const auto label = FromLatin1("markdown-example.md"); auto renderer = std::make_shared(); auto fixture = PreparedFixture(); if (!PrepareFixture( DefaultFixturePath(label), label, renderer, &fixture)) { Check(false, label + FromLatin1(" fixture preparation"), ok); return; } auto window = Ui::RpWindow(); window.setGeometry(QRect(0, 0, 520, 240)); window.show(); FlushPendingWidgetEvents(); auto preview = CreateMarkdownPreviewWidget( window.body(), std::move(fixture.prepared), renderer, [](Event) { }); preview->setGeometry(QRect(QPoint(), window.body()->size())); preview->show(); FlushPendingWidgetEvents(); const auto scroll = FindChildObject(preview.get()); const auto body = FindChildObject(preview.get()); Check( scroll != nullptr, label + FromLatin1(" preview scroll area"), ok); Check( body != nullptr, label + FromLatin1(" preview body widget"), ok); if (!scroll || !body) { return; } const auto initialTop = scroll->scrollTop(); const auto anchorId = u"headings"_q; Check( scroll->scrollTopMax() > 0, label + FromLatin1(" preview can scroll"), ok); Check( body->anchorTop(anchorId) > initialTop, label + FromLatin1(" headings anchor starts below viewport top"), ok); if (!(scroll->scrollTopMax() > 0) || !(body->anchorTop(anchorId) > initialTop)) { return; } Check( ScrollMarkdownPreviewToAnchor(preview.get(), anchorId), label + FromLatin1(" anchor scroll request"), ok); FlushPendingWidgetEvents(); Check( scroll->scrollTop() == std::min(body->anchorTop(anchorId), scroll->scrollTopMax()), label + FromLatin1(" anchor scroll aligns to viewport top"), ok); } void CheckArticleHorizontalRelayoutRegression(bool *ok) { const auto label = FromLatin1("generated-horizontal-relayout-regression.md"); const auto parsed = ParseMarkdownForIv( QByteArray(R"(# Horizontal resize regression heading that wraps when narrowed This body paragraph also needs to reflow after a horizontal resize so the article must rebuild later block offsets instead of leaving them behind. $$ \int_0^1 x^2 \, dx = \frac{1}{3} $$ ThisIsALongUnbrokenStringToTestWrappingBehavior_ABCD1234EFGH5678IJKL )"), ParseOptions{ label }); Check( parsed.ok, label + FromLatin1(" parse failed: ") + parsed.error, ok); if (!parsed.ok) { return; } const auto &topLevelBlocks = parsed.document.document.children; Check( topLevelBlocks.size() == 4, label + FromLatin1(" parse top-level block count"), ok); if (topLevelBlocks.size() != 4) { return; } Check( topLevelBlocks[0].kind == NodeKind::Heading, label + FromLatin1(" parse heading block kind"), ok); Check( topLevelBlocks[1].kind == NodeKind::Paragraph, label + FromLatin1(" parse body paragraph block kind"), ok); Check( topLevelBlocks[2].kind == NodeKind::DisplayMath, label + FromLatin1(" parse display math block kind"), ok); Check( topLevelBlocks[3].kind == NodeKind::Paragraph, label + FromLatin1(" parse trailing paragraph block kind"), ok); auto renderer = std::make_shared(); auto prepared = PrepareParsedDocumentForTest( parsed.document, label, renderer); Check( !prepared.failure.failed(), label + FromLatin1(" prepare failure: ") + PrepareFailureReason(prepared.failure), ok); if (prepared.failure.failed()) { return; } Check( prepared.blocks.blocks.size() == 4, label + FromLatin1(" prepared top-level block count"), ok); if (prepared.blocks.blocks.size() != 4) { return; } const auto wideWidth = 640; const auto narrowWidth = 280; auto wideHeight = 0; auto article = BuildArticleForTest( std::move(prepared), renderer, wideWidth, &wideHeight); const auto wideImage = PaintArticleForTest( article.get(), wideWidth, wideHeight); Check( HasPaintedPixels(wideImage), label + FromLatin1(" wide paint produced pixels"), ok); const auto wideFinalBounds = SegmentHitBounds( article.get(), wideWidth, wideHeight, 3); Check( wideFinalBounds.has_value(), label + FromLatin1(" wide final segment hit bounds"), ok); const auto narrowHeight = article->resizeGetHeight(narrowWidth); const auto narrowImage = PaintArticleForTest( article.get(), narrowWidth, narrowHeight); Check( HasPaintedPixels(narrowImage), label + FromLatin1(" narrow paint produced pixels"), ok); Check( narrowHeight > wideHeight, label + FromLatin1(" narrow relayout height grows"), ok); auto segmentBounds = std::vector>(); segmentBounds.reserve(4); for (auto segmentIndex = 0; segmentIndex != 4; ++segmentIndex) { segmentBounds.push_back(SegmentHitBounds( article.get(), narrowWidth, narrowHeight, segmentIndex)); Check( segmentBounds.back().has_value(), label + FromLatin1(" segment hit bounds ") + QString::number(segmentIndex), ok); } auto haveAllSegmentBounds = true; for (const auto &bounds : segmentBounds) { if (!bounds.has_value()) { haveAllSegmentBounds = false; break; } } if (!haveAllSegmentBounds) { return; } for (auto segmentIndex = 1; segmentIndex != 4; ++segmentIndex) { const auto &previousBounds = *segmentBounds[segmentIndex - 1]; const auto ¤tBounds = *segmentBounds[segmentIndex]; Check( currentBounds.top() > previousBounds.top(), label + FromLatin1(" segment document order ") + QString::number(segmentIndex), ok); Check( currentBounds.top() > previousBounds.bottom(), label + FromLatin1(" segment vertical separation ") + QString::number(segmentIndex), ok); } const auto &finalBounds = *segmentBounds.back(); if (wideFinalBounds) { Check( finalBounds.height() > wideFinalBounds->height(), label + FromLatin1(" long final segment wraps when narrowed"), ok); } Check( finalBounds.height() > st::defaultMarkdown.body.lineHeight, label + FromLatin1(" long final segment spans multiple lines"), ok); auto lookupFlags = Ui::Text::StateRequest::Flags(); lookupFlags |= Ui::Text::StateRequest::Flag::LookupSymbol; auto finalProbePoint = std::optional(); for (auto y = finalBounds.top(); (y <= finalBounds.bottom()) && !finalProbePoint; ++y) { for (auto x = finalBounds.left(); x <= finalBounds.right(); ++x) { const auto hit = article->hitTest(QPoint(x, y), lookupFlags); if (hit.valid() && hit.direct && (hit.segmentIndex == 3)) { finalProbePoint = QPoint(x, y); break; } } } Check( finalProbePoint.has_value(), label + FromLatin1(" final segment direct probe point"), ok); if (!finalProbePoint) { return; } const auto finalHit = article->hitTest(*finalProbePoint, lookupFlags); Check( finalBounds.contains(*finalProbePoint), label + FromLatin1(" final probe point inside final segment bounds"), ok); Check( finalHit.valid() && finalHit.direct && (finalHit.segmentIndex == 3), label + FromLatin1(" final segment hit after relayout"), ok); } [[nodiscard]] int RunTests(int argc, char **argv) { auto args = ParseArgs(argc, argv); if (!args.ok) { PrintError(args.error); return 1; } if (args.inlineHtml) { auto ok = true; CheckInlineHtmlCoverage(args.dump, &ok); CheckInlineHtmlPrepareCoverage(&ok); return ok ? 0 : 1; } if (args.markdownPath.isEmpty()) { args.markdownPath = DefaultFixturePath(FromLatin1("markdown-example.md")); } if (args.latexMarkdownPath.isEmpty()) { args.latexMarkdownPath = DefaultFixturePath( FromLatin1("latex-markdown-test.md")); } auto fixtureRenderer = std::make_shared(); auto markdownFixture = PreparedFixture(); if (!PrepareFixture( args.markdownPath, FromLatin1("markdown-example.md"), fixtureRenderer, &markdownFixture)) { return 1; } if (args.dump) { PrintLine(DumpForDebug(markdownFixture.parsed)); } auto latexFixture = PreparedFixture(); if (!PrepareFixture( args.latexMarkdownPath, FromLatin1("latex-markdown-test.md"), fixtureRenderer, &latexFixture)) { return 1; } if (args.dump) { PrintLine(DumpForDebug(latexFixture.parsed)); } const auto &markdown = markdownFixture.parsed; const auto &latex = latexFixture.parsed; auto ok = true; Check( markdown.stats.cmarkNodeCount == 562, FromLatin1("markdown-example.md cmark node count"), &ok); Check( CountFormulas(markdown, MathKind::Inline) == 1, FromLatin1("markdown-example.md inline formula count"), &ok); Check( CountFormulas(markdown, MathKind::Display) == 1, FromLatin1("markdown-example.md display formula count"), &ok); Check( CountDisplayMathNodes(markdown.document) == 1, FromLatin1("markdown-example.md display math node count"), &ok); Check( FindNodeByKindAndRange( markdown.document, NodeKind::DisplayMath, 281, 1, 283, 2) != nullptr, FromLatin1("markdown-example.md display formula range"), &ok); Check( CountNodesByKindAndRange( markdown.document, NodeKind::Paragraph, 281, 1, 283, 2) == 0, FromLatin1("markdown-example.md duplicate display paragraph removed"), &ok); Check( HasKind(markdown.document, NodeKind::Table), FromLatin1("markdown-example.md table coverage"), &ok); Check( HasKind(markdown.document, NodeKind::Strike), FromLatin1("markdown-example.md strikethrough coverage"), &ok); Check( HasTaskState(markdown.document, TaskState::Checked), FromLatin1("markdown-example.md checked task"), &ok); Check( HasTaskState(markdown.document, TaskState::Unchecked), FromLatin1("markdown-example.md unchecked task"), &ok); auto markdownTables = std::vector(); CollectTables(markdown.document, &markdownTables); Check( int(markdownTables.size()) >= 2, FromLatin1("markdown-example.md table count"), &ok); if (markdownTables.size() >= 2) { const auto &firstTable = *markdownTables[0]; const auto &secondTable = *markdownTables[1]; Check( HasTableHeaderRow(firstTable), FromLatin1("markdown-example.md first table header row"), &ok); Check( TableHeaderRowCount(firstTable) == 1, FromLatin1("markdown-example.md first table header row count"), &ok); Check( TableColumnCount(firstTable) == 3, FromLatin1("markdown-example.md first table column count"), &ok); Check( HasSequentialTableColumns(firstTable), FromLatin1("markdown-example.md first table column order"), &ok); Check( HasTableAlignments( secondTable, { TableAlignment::Left, TableAlignment::Center, TableAlignment::Right, }), FromLatin1("markdown-example.md second table alignments"), &ok); } CheckFixtureSemanticCoverage(markdown, args.markdownPath, &ok); Check( latex.stats.cmarkNodeCount == 532, FromLatin1("latex-markdown-test.md cmark node count"), &ok); Check( CountFormulas(latex, MathKind::Inline) == 100, FromLatin1("latex-markdown-test.md inline formula count"), &ok); Check( CountFormulas(latex, MathKind::Display) == 31, FromLatin1("latex-markdown-test.md display formula count"), &ok); Check( CountDisplayMathNodes(latex.document) == 31, FromLatin1("latex-markdown-test.md display math node count"), &ok); Check( FindNodeByPathAndRange( latex.document, { NodeKind::Document, NodeKind::List, NodeKind::ListItem, NodeKind::DisplayMath, }, 299, 4, 301, 5) != nullptr, FromLatin1("latex-markdown-test.md list display formula nested"), &ok); Check( FindNodeByPathAndRange( latex.document, { NodeKind::Document, NodeKind::DisplayMath, }, 299, 4, 301, 5) == nullptr, FromLatin1("latex-markdown-test.md list display formula not hoisted"), &ok); Check( FindNodeByPathAndRange( latex.document, { NodeKind::Document, NodeKind::Blockquote, NodeKind::DisplayMath, }, 307, 3, 309, 4) != nullptr, FromLatin1("latex-markdown-test.md blockquote display formula nested"), &ok); Check( FindNodeByPathAndRange( latex.document, { NodeKind::Document, NodeKind::DisplayMath, }, 307, 3, 309, 4) == nullptr, FromLatin1("latex-markdown-test.md blockquote display formula not hoisted"), &ok); Check( HasKind(latex.document, NodeKind::Table), FromLatin1("latex-markdown-test.md table coverage"), &ok); auto latexTables = std::vector(); CollectTables(latex.document, &latexTables); Check( !latexTables.empty(), FromLatin1("latex-markdown-test.md table count"), &ok); if (!latexTables.empty()) { const auto &table = *latexTables[0]; Check( HasTableHeaderRow(table), FromLatin1("latex-markdown-test.md table header row"), &ok); Check( TableColumnCount(table) == 3, FromLatin1("latex-markdown-test.md table column count"), &ok); Check( HasSequentialTableColumns(table), FromLatin1("latex-markdown-test.md table column order"), &ok); } const auto tableMath = FindNodeByKindAndLineRange( latex.document, NodeKind::Table, 313, 318); Check( tableMath != nullptr, FromLatin1("latex-markdown-test.md table range"), &ok); if (tableMath) { Check( !HasKind(*tableMath, NodeKind::DisplayMath), FromLatin1("latex-markdown-test.md table display math stays literal"), &ok); Check( !HasKind(*tableMath, NodeKind::InlineMath), FromLatin1("latex-markdown-test.md table inline math stays literal"), &ok); } Check( CountFormulasInLineRange(latex, MathKind::Inline, 315, 318) == 12, FromLatin1("latex-markdown-test.md table inline formulas preserved"), &ok); Check( CountFormulasInLineRange(latex, MathKind::Display, 315, 318) == 0, FromLatin1("latex-markdown-test.md table display formulas absent"), &ok); Check( HasFormulaOnLine(latex, 71, FromLatin1("\\int_0^1 x \\, dx")), FromLatin1("latex-markdown-test.md line 71 inline formula"), &ok); Check( HasFormulaOnLine( latex, 259, FromLatin1( "\\mathbb{E}[X] = \\int_{-\\infty}^{\\infty} x f(x) \\, dx")), FromLatin1("latex-markdown-test.md line 259 inline formula"), &ok); const auto headerMath = FindNodeByKindAndLineRange( latex.document, NodeKind::Heading, 322, 322); Check( headerMath != nullptr, FromLatin1("latex-markdown-test.md heading range"), &ok); if (headerMath) { Check( !HasKind(*headerMath, NodeKind::DisplayMath), FromLatin1("latex-markdown-test.md heading display math stays inline"), &ok); } Check( HasFormulaOnLine(latex, 322, FromLatin1("ax^2 + bx + c = 0")), FromLatin1("latex-markdown-test.md line 322 inline formula"), &ok); const auto strongMath = FindNodeByKindAndLineRange( latex.document, NodeKind::Strong, 326, 326); Check( strongMath != nullptr, FromLatin1("latex-markdown-test.md strong range"), &ok); if (strongMath) { Check( !HasKind(*strongMath, NodeKind::DisplayMath), FromLatin1("latex-markdown-test.md strong display math stays inline"), &ok); Check( !HasKind(*strongMath, NodeKind::InlineMath), FromLatin1("latex-markdown-test.md strong inline math stays literal"), &ok); } Check( HasFormulaOnLine(latex, 326, FromLatin1("E = mc^2")), FromLatin1("latex-markdown-test.md strong inline formula preserved"), &ok); const auto emphasisMath = FindNodeByKindAndLineRange( latex.document, NodeKind::Emphasis, 328, 328); Check( emphasisMath != nullptr, FromLatin1("latex-markdown-test.md emphasis range"), &ok); if (emphasisMath) { Check( !HasKind(*emphasisMath, NodeKind::DisplayMath), FromLatin1("latex-markdown-test.md emphasis display math stays inline"), &ok); Check( !HasKind(*emphasisMath, NodeKind::InlineMath), FromLatin1("latex-markdown-test.md emphasis inline math stays literal"), &ok); } Check( HasFormulaOnLine(latex, 328, FromLatin1("\\pi \\approx 3.14")), FromLatin1("latex-markdown-test.md emphasis inline formula preserved"), &ok); Check( HasFormulaOnLine(latex, 381, FromLatin1("a\\,b")), FromLatin1("latex-markdown-test.md line 381 inline formula"), &ok); Check( HasFormulaOnLine(latex, 383, FromLatin1("a\\:b")), FromLatin1("latex-markdown-test.md line 383 inline formula"), &ok); Check( HasFormulaOnLine(latex, 385, FromLatin1("a\\;b")), FromLatin1("latex-markdown-test.md line 385 inline formula"), &ok); Check( HasFormulaOnLine(latex, 391, FromLatin1("a\\!b")), FromLatin1("latex-markdown-test.md line 391 inline formula"), &ok); const auto fencedCode = FindNodeByKindAndLineRange( latex.document, NodeKind::CodeBlock, 332, 334); Check( fencedCode != nullptr, FromLatin1("latex-markdown-test.md fenced code range"), &ok); if (fencedCode) { Check( !HasKind(*fencedCode, NodeKind::DisplayMath), FromLatin1("latex-markdown-test.md fenced code display math stays literal"), &ok); Check( !HasKind(*fencedCode, NodeKind::InlineMath), FromLatin1("latex-markdown-test.md fenced code inline math stays literal"), &ok); } const auto inlineCode = FindNodeByKindAndLineRange( latex.document, NodeKind::InlineCode, 336, 336); Check( inlineCode != nullptr, FromLatin1("latex-markdown-test.md inline code range"), &ok); if (inlineCode) { Check( !HasKind(*inlineCode, NodeKind::DisplayMath), FromLatin1("latex-markdown-test.md inline code display math stays literal"), &ok); Check( !HasKind(*inlineCode, NodeKind::InlineMath), FromLatin1("latex-markdown-test.md inline code math stays literal"), &ok); } Check( CountFormulasInLineRange(latex, MathKind::Inline, 332, 336) == 0, FromLatin1("latex-markdown-test.md code inline formulas excluded"), &ok); Check( CountFormulasInLineRange(latex, MathKind::Display, 332, 336) == 0, FromLatin1("latex-markdown-test.md code display formulas excluded"), &ok); Check( !HasFormulaInLineRange(latex, 281, 281), FromLatin1("latex-markdown-test.md line 281 exclusion"), &ok); Check( HasFormulaOnLine(latex, 285, FromLatin1("5x + 3")), FromLatin1("latex-markdown-test.md line 285 formula"), &ok); Check( !HasFormulaInLineRange(latex, 332, 340), FromLatin1("latex-markdown-test.md lines 332-340 exclusions"), &ok); CheckInlineTextObjectPrepareCoverage(&ok); CheckNativeInstantViewPrepareCoverage(&ok); CheckCodeBlockTrailingNewlineTrim(&ok); CheckCodeBlockSelectionExportCoverage(&ok); CheckCodeBlockAsyncSyntaxHighlightCoverage(&ok); CheckPrepareCoverage(markdownFixture, latexFixture, &ok); CheckPrepareLinkClassification(markdownFixture.path, &ok); CheckPreparedExternalLinkCoverage(&ok); CheckDetailsSummaryHitCoverage(&ok); CheckTableFormulaTextSizeCoverage(&ok); CheckArticleRenderSmoke(markdownFixture, latexFixture, &ok); CheckInlineTextObjectArticleCoverage(&ok); CheckArticleRasterRegressionCoverage(&ok); CheckAnchorScrollAlignmentCoverage(&ok); CheckArticleHorizontalRelayoutRegression(&ok); CheckNativeInstantViewArticleCoverage(&ok); CheckInlineHtmlCoverage(args.dump, &ok); CheckInlineHtmlPrepareCoverage(&ok); CheckValidationEdges(&ok); return ok ? 0 : 1; } } // namespace int main(int argc, char **argv) { QCoreApplication::setAttribute(Qt::AA_Use96Dpi); auto application = QApplication(argc, argv); (void)application; style::SetDevicePixelRatio(1); style::StartManager(style::kScaleDefault); const auto result = RunTests(argc, argv); style::StopManager(); return result; } namespace crl { rpl::producer<> on_main_update_requests() { return rpl::never<>(); } } // namespace crl