Add markdown viewer header, sharing.

This commit is contained in:
John Preston
2026-05-06 14:31:45 +04:00
parent cd540ea795
commit 67764c803d
15 changed files with 817 additions and 77 deletions
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/abstract_box.h" // Ui::show().
#include "chat_helpers/ttl_media_layer_widget.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "core/core_settings.h"
#include "core/mime_type.h"
#include "data/data_document.h"
@@ -251,7 +252,17 @@ void ResolveDocument(
if (!openImageInApp()) {
const auto path = document->filepath(true);
if (!path.isEmpty()) {
if (!Core::App().iv().showMarkdown(path)) {
auto context = QVariant();
if (item) {
auto clickHandlerContext = ClickHandlerContext();
clickHandlerContext.itemId = item->fullId();
if (controller) {
clickHandlerContext.sessionWindow = controller;
clickHandlerContext.show = controller->uiShow();
}
context = QVariant::fromValue(clickHandlerContext);
}
if (!Core::App().iv().showMarkdown(path, context)) {
LaunchWithWarning(path, item);
}
} else if (document->status == FileReady
+2 -2
View File
@@ -175,7 +175,7 @@ defaultMarkdownBodyStyle: TextStyle(historyTextStyle) {
lineHeight: 24px;
}
defaultMarkdownCodeStyle: TextStyle(defaultMarkdownBodyStyle) {
font: font(15px);
font: font(14px);
lineHeight: 22px;
}
defaultMarkdownDisplayMathFallbackStyle: TextStyle(defaultMarkdownCodeStyle) {
@@ -200,7 +200,7 @@ defaultMarkdownList: MarkdownList {
continuationIndent: 22px;
markerWidth: 22px;
markerSkip: 8px;
bulletRadius: 3px;
bulletRadius: 2px;
bulletLeftShift: 0px;
bulletFg: windowFg;
taskCheck: defaultCheck;
@@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtCore/QJsonValue>
#include <QtCore/QFile>
#include <QtGui/QGuiApplication>
#include <QtGui/QKeySequence>
#include <QtGui/QPainter>
#include <QtGui/QWindow>
#include <charconv>
@@ -689,6 +690,10 @@ void Controller::createWebview(const Webview::StorageId &storageId) {
return base::EventFilterResult::Continue;
}
const auto event = static_cast<QKeyEvent*>(e.get());
if (event->matches(QKeySequence::Close)) {
close();
return base::EventFilterResult::Cancel;
}
if (event->modifiers() & Qt::ControlModifier) {
if (event->key() == Qt::Key_Plus
|| event->key() == Qt::Key_Equal) {
+182 -33
View File
@@ -26,6 +26,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_thread.h"
#include "data/data_web_page.h"
#include "data/data_user.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "info/profile/info_profile_values.h"
#include "iv/markdown/iv_markdown_controller.h"
@@ -54,9 +56,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_session_controller.h"
#include "window/window_session_controller_link_info.h"
#include <QtCore/QByteArray>
#include <QtCore/QFileInfo>
#include <QtGui/QGuiApplication>
#include <QtGui/QWindow>
#include <optional>
namespace Iv {
namespace {
@@ -278,6 +284,119 @@ private:
};
struct MarkdownMessageContext {
ClickHandlerContext clickHandlerContext;
base::weak_ptr<Window::SessionController> sessionWindow;
};
struct LocalMarkdownTarget {
QString key;
QString path;
QString sourceName;
QString fragment;
};
[[nodiscard]] QString NormalizeLocalMarkdownFragment(QString fragment) {
fragment = QString::fromUtf8(
QByteArray::fromPercentEncoding(fragment.toUtf8()));
fragment = fragment.trimmed().toLower();
while (fragment.startsWith(QChar('#'))) {
fragment.remove(0, 1);
}
return fragment;
}
[[nodiscard]] LocalMarkdownTarget ParseLocalMarkdownTarget(QString path) {
auto sourcePath = path;
auto fragment = QString();
if (!QFileInfo(sourcePath).exists()) {
const auto hash = sourcePath.lastIndexOf(QChar('#'));
const auto candidate = (hash > 0) ? sourcePath.mid(0, hash) : QString();
if (!candidate.isEmpty() && QFileInfo(candidate).exists()) {
fragment = NormalizeLocalMarkdownFragment(sourcePath.mid(hash + 1));
sourcePath = candidate;
}
}
const auto info = QFileInfo(sourcePath);
if (!info.exists()) {
return {
.key = path,
.path = std::move(path),
};
}
auto result = LocalMarkdownTarget{
.key = info.absoluteFilePath(),
.path = info.absoluteFilePath(),
.sourceName = info.fileName(),
.fragment = std::move(fragment),
};
if (!result.fragment.isEmpty()) {
result.path += u"#"_q + result.fragment;
}
return result;
}
[[nodiscard]] auto ExtractMarkdownMessageContext(const QVariant &context) {
if (!context.isValid() || !context.canConvert<ClickHandlerContext>()) {
return std::optional<MarkdownMessageContext>();
}
const auto clickHandlerContext = context.value<ClickHandlerContext>();
return std::make_optional(MarkdownMessageContext{
.clickHandlerContext = clickHandlerContext,
.sessionWindow = clickHandlerContext.sessionWindow,
});
}
[[nodiscard]] Main::Session *ResolveMarkdownSession(
const MarkdownMessageContext &context) {
if (const auto controller = context.sessionWindow.get()) {
return &controller->session();
}
return nullptr;
}
[[nodiscard]] HistoryItem *ResolveMarkdownItem(
const MarkdownMessageContext &context) {
const auto session = ResolveMarkdownSession(context);
const auto itemId = context.clickHandlerContext.itemId;
return (session && itemId) ? session->data().message(itemId) : nullptr;
}
[[nodiscard]] bool CanShareMarkdownItem(not_null<HistoryItem*> item) {
const auto peer = item->history()->peer;
return peer->allowsForwarding() && !item->forbidsForward();
}
[[nodiscard]] Markdown::OpenOptions PrepareLocalMarkdownOptions(
QVariant context) {
auto options = Markdown::OpenOptions{
.viewerKind = Markdown::ViewerKind::LocalFile,
.clickHandlerContext = std::move(context),
};
const auto messageContext = ExtractMarkdownMessageContext(
options.clickHandlerContext);
const auto item = messageContext
? ResolveMarkdownItem(*messageContext)
: nullptr;
if (item && CanShareMarkdownItem(not_null{ item })) {
options.share = [context = *messageContext](
std::shared_ptr<Ui::Show> show) {
const auto session = ResolveMarkdownSession(context);
const auto itemId = context.clickHandlerContext.itemId;
const auto current = (session && itemId)
? session->data().message(itemId)
: nullptr;
if (!show || !current || !CanShareMarkdownItem(not_null{ current })) {
return;
}
FastShareMessage(
Main::MakeSessionShow(show, not_null{ session }),
not_null{ current });
};
}
return options;
}
} // namespace
class Shown final : public base::has_weak_ptr {
@@ -322,7 +441,7 @@ private:
};
struct FileStream {
not_null<DocumentData*> document;
std::unique_ptr<Media::Streaming::Loader> loader;
std::unique_ptr<::Media::Streaming::Loader> loader;
std::vector<PartRequest> requests;
std::string mime;
rpl::lifetime lifetime;
@@ -339,6 +458,9 @@ private:
QString title,
QString initialFragment,
not_null<WebPageData*> page);
[[nodiscard]] Markdown::OpenOptions markdownOpenOptions(
QString initialFragment,
not_null<WebPageData*> page) const;
void showWindowed(Prepared result, Source source, bool refresh);
void showHtmlWindowed(Prepared result, bool refresh);
@@ -352,7 +474,8 @@ private:
not_null<WebPageData*> page) const;
[[nodiscard]] bool activateMarkdownMedia(
const Markdown::MediaActivation &activation,
Qt::MouseButton button) const;
Qt::MouseButton button,
const QVariant &clickHandlerContext) const;
[[nodiscard]] ::Data::FileOrigin fileOrigin(
not_null<WebPageData*> page) const;
@@ -361,10 +484,10 @@ private:
void streamFile(FileStream &file, Webview::DataRequest request);
void processPartInFile(
FileStream &file,
Media::Streaming::LoadedPart &&part);
::Media::Streaming::LoadedPart &&part);
bool finishRequestWithPart(
PartRequest &request,
const Media::Streaming::LoadedPart &part);
const ::Media::Streaming::LoadedPart &part);
void streamMap(QString params, Webview::DataRequest request);
void sendEmbed(QByteArray hash, Webview::DataRequest request);
@@ -640,6 +763,33 @@ void Shown::createController() {
}, _controller->lifetime());
}
Markdown::OpenOptions Shown::markdownOpenOptions(
QString initialFragment,
not_null<WebPageData*> page) const {
const auto clickHandlerContext = std::make_shared<QVariant>();
auto options = Markdown::OpenOptions{
.sourceName = page->displayedSiteName(),
.sourceUrl = page->url,
.initialFragment = std::move(initialFragment),
.viewerKind = Markdown::ViewerKind::InstantView,
.clickHandlerContextRef = clickHandlerContext,
.activateMedia = [=](
const Markdown::MediaActivation &activation,
Qt::MouseButton button) {
return activateMarkdownMedia(activation, button, *clickHandlerContext);
},
};
if (!page->url.isEmpty()) {
options.share = [=, url = page->url](std::shared_ptr<Ui::Show> show) {
if (!show) {
return;
}
FastShareLink(Main::MakeSessionShow(show, _session), url);
};
}
return options;
}
void Shown::createMarkdownController(
Markdown::MarkdownArticleContent content,
QString title,
@@ -647,14 +797,7 @@ void Shown::createMarkdownController(
not_null<WebPageData*> page) {
Expects(!_markdownController);
auto options = Markdown::OpenOptions{
.initialFragment = std::move(initialFragment),
.activateMedia = [=](
const Markdown::MediaActivation &activation,
Qt::MouseButton button) {
return activateMarkdownMedia(activation, button);
},
};
auto options = markdownOpenOptions(std::move(initialFragment), page);
_markdownController = std::make_unique<Markdown::Controller>(
_delegate,
std::move(content),
@@ -717,15 +860,8 @@ void Shown::showMarkdownWindowed(
QString initialFragment,
not_null<WebPageData*> page) {
_controller = nullptr;
auto options = markdownOpenOptions(std::move(initialFragment), page);
if (_markdownController) {
auto options = Markdown::OpenOptions{
.initialFragment = std::move(initialFragment),
.activateMedia = [=](
const Markdown::MediaActivation &activation,
Qt::MouseButton button) {
return activateMarkdownMedia(activation, button);
},
};
_markdownController->update(
std::move(content),
std::move(title),
@@ -746,7 +882,8 @@ std::shared_ptr<Markdown::MediaRuntime> Shown::createMediaRuntime(
bool Shown::activateMarkdownMedia(
const Markdown::MediaActivation &activation,
Qt::MouseButton button) const {
Qt::MouseButton button,
const QVariant &clickHandlerContext) const {
if (button != Qt::LeftButton && button != Qt::MiddleButton) {
return false;
}
@@ -757,7 +894,7 @@ bool Shown::activateMarkdownMedia(
if (activation.url.isEmpty()) {
return false;
}
HiddenUrlClickHandler::Open(activation.url);
HiddenUrlClickHandler::Open(activation.url, clickHandlerContext);
return true;
case Markdown::MediaActivationKind::Photo:
if (!activation.photo) {
@@ -862,7 +999,7 @@ void Shown::streamFile(
}).first->second;
file.loader->parts(
) | rpl::on_next([=](Media::Streaming::LoadedPart &&part) {
) | rpl::on_next([=](::Media::Streaming::LoadedPart &&part) {
const auto i = _streams.find(documentId);
Assert(i != end(_streams));
processPartInFile(i->second, std::move(part));
@@ -872,7 +1009,7 @@ void Shown::streamFile(
}
void Shown::streamFile(FileStream &file, Webview::DataRequest request) {
constexpr auto kPart = Media::Streaming::Loader::kPartSize;
constexpr auto kPart = ::Media::Streaming::Loader::kPartSize;
const auto size = file.document->size;
const auto last = int((size + kPart - 1) / kPart);
const auto from = int(std::min(int64(request.offset), size) / kPart);
@@ -938,7 +1075,7 @@ QByteArray Shown::readFile(
void Shown::processPartInFile(
FileStream &file,
Media::Streaming::LoadedPart &&part) {
::Media::Streaming::LoadedPart &&part) {
for (auto i = begin(file.requests); i != end(file.requests);) {
if (finishRequestWithPart(*i, part)) {
auto done = base::take(*i);
@@ -957,16 +1094,16 @@ void Shown::processPartInFile(
bool Shown::finishRequestWithPart(
PartRequest &request,
const Media::Streaming::LoadedPart &part) {
const ::Media::Streaming::LoadedPart &part) {
const auto offset = part.offset;
if (offset == Media::Streaming::LoadedPart::kFailedOffset) {
if (offset == ::Media::Streaming::LoadedPart::kFailedOffset) {
request.data = QByteArray();
return true;
} else if (offset < request.offset
|| offset >= request.offset + request.data.size()) {
return false;
}
constexpr auto kPart = Media::Streaming::Loader::kPartSize;
constexpr auto kPart = ::Media::Streaming::Loader::kPartSize;
const auto copy = std::min(
int(part.bytes.size()),
int(request.data.size() - (offset - request.offset)));
@@ -1493,20 +1630,30 @@ void Instance::showTonSite(
bool Instance::showMarkdown(
const QString &path,
QVariant context) {
auto i = _markdowns.find(path);
const auto target = ParseLocalMarkdownTarget(path);
auto options = PrepareLocalMarkdownOptions(context);
if (!target.sourceName.isEmpty()) {
options.sourceName = target.sourceName;
options.sourcePath = target.key;
}
options.initialFragment = target.fragment;
auto i = _markdowns.find(target.key);
if (i == end(_markdowns)) {
if (auto controller = Markdown::TryOpenLocalFile(_delegate, path)) {
if (auto controller = Markdown::TryOpenLocalFile(
_delegate,
target.path,
std::move(options))) {
controller->events() | rpl::on_next([=](Markdown::Event event) {
using Type = Markdown::Event::Type;
switch (event.type) {
case Type::Close:
_markdowns.take(path);
_markdowns.take(target.key);
break;
case Type::Quit:
Shortcuts::Launch(Shortcuts::Command::Quit);
break;
case Type::OpenFile:
if (!showMarkdown(event.url)) {
if (!showMarkdown(event.url, event.context)) {
DEBUG_LOG(("Native Markdown IV: "
"failed local markdown link: %1"
).arg(event.url));
@@ -1515,10 +1662,12 @@ bool Instance::showMarkdown(
}
}, controller->lifetime());
i = _markdowns.emplace(path, std::move(controller)).first;
i = _markdowns.emplace(target.key, std::move(controller)).first;
} else {
return false;
}
} else {
i->second->updateOptions(std::move(options));
}
i->second->activate();
return true;
@@ -24,6 +24,13 @@ constexpr auto kCodeTabColumns = 4;
constexpr auto kCodeTrailingGuard = 0x2060;
const auto kPhotoCopyLabel = u"Photo"_q;
[[nodiscard]] int SingleDigitOrderedMarkerWidth(
const style::Markdown &markdown) {
return std::max(
markdown.body.font->width(u"8."_q),
markdown.body.font->width(u"8)"_q));
}
[[nodiscard]] QString CodeBlockDisplayText(const QString &text) {
auto result = QString();
result.reserve(text.size());
@@ -210,8 +217,9 @@ QPoint BulletMarkerCenter(
const style::Markdown &markdown) {
const auto &list = markdown.list;
const auto lineHeight = TextLineHeight(markdown.body);
const auto markerWidth = SingleDigitOrderedMarkerWidth(markdown);
return QPoint(
left + list.markerWidth - list.bulletLeftShift - (lineHeight / 2),
left + list.markerWidth - list.bulletLeftShift - ((markerWidth + 1) / 2),
top + (lineHeight / 2));
}
@@ -2,12 +2,14 @@
#include <QtCore/QSize>
#include <QtCore/QString>
#include <QtCore/QVariant>
#include <functional>
#include <memory>
namespace Ui {
class DynamicImage;
class Show;
} // namespace Ui
namespace Iv {
@@ -53,11 +55,23 @@ struct MediaActivation {
std::shared_ptr<PhotoRuntime> photo;
};
enum class ViewerKind {
Auto,
LocalFile,
InstantView,
};
struct OpenOptions {
QString sourceName;
QString sourcePath;
QString sourceUrl;
QString initialFragment;
ViewerKind viewerKind = ViewerKind::Auto;
Iv::Delegate *delegate = nullptr;
QVariant clickHandlerContext;
std::shared_ptr<QVariant> clickHandlerContextRef;
std::function<void()> openSource;
std::function<void(std::shared_ptr<Ui::Show>)> share;
std::function<bool(const MediaActivation &, Qt::MouseButton)> activateMedia;
};
@@ -77,6 +91,7 @@ struct Event {
};
Type type = Type::Close;
QString url;
QVariant context;
};
} // namespace Iv::Markdown
@@ -1,18 +1,34 @@
#include "iv/markdown/iv_markdown_controller.h"
#include "base/event_filter.h"
#include "base/weak_ptr.h"
#include "core/credits_amount.h"
#include "core/click_handler_types.h"
#include "iv/markdown/iv_markdown_parse.h"
#include "iv/markdown/iv_markdown_view.h"
#include "iv/iv_delegate_impl.h"
#include "core/file_utilities.h"
#include "lang/lang_keys.h"
#include "logs.h"
#include "ui/layers/layer_manager.h"
#include "ui/layers/show.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/rp_window.h"
#include "ui/wrap/fade_wrap.h"
#include "styles/style_iv.h"
#include "styles/style_menu_icons.h"
#include "styles/palette.h"
#include "styles/style_window.h"
#include <QtCore/QElapsedTimer>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QUrl>
#include <QtGui/QKeyEvent>
#include <QtGui/QKeySequence>
#include <QtGui/QPainter>
#include <algorithm>
@@ -30,12 +46,80 @@ constexpr auto kZoomStep = int(10);
not_null<Delegate*> delegate,
const QString &title) {
options.delegate = delegate;
if (options.sourceName.isEmpty()) {
options.sourceName = title;
}
Q_UNUSED(title);
return options;
}
[[nodiscard]] ViewerKind ResolveViewerKind(const OpenOptions &options) {
return (options.viewerKind != ViewerKind::Auto)
? options.viewerKind
: options.sourcePath.isEmpty()
? ViewerKind::InstantView
: ViewerKind::LocalFile;
}
[[nodiscard]] QString SubtitleText(
const OpenOptions &options,
const QString &title) {
if (!options.sourceName.trimmed().isEmpty()) {
return options.sourceName.trimmed();
}
const auto host = QUrl(options.sourceUrl).host().trimmed();
return !host.isEmpty() ? host : title.trimmed();
}
[[nodiscard]] QString OpenSourceLabel(ViewerKind kind) {
return (kind == ViewerKind::InstantView)
? tr::lng_iv_open_in_browser(tr::now)
: tr::lng_markdown_preview_open_file(tr::now);
}
[[nodiscard]] const style::icon *OpenSourceIcon(ViewerKind kind) {
return (kind == ViewerKind::InstantView)
? &st::menuIconIpAddress
: &st::menuIconFile;
}
[[nodiscard]] QVariant ExtendClickHandlerContext(
QVariant context,
const std::shared_ptr<Ui::Show> &show) {
if (!show) {
return context;
} else if (!context.isValid()
|| context.canConvert<ClickHandlerContext>()) {
auto clickContext = context.isValid()
? context.value<ClickHandlerContext>()
: ClickHandlerContext();
clickContext.show = show;
return QVariant::fromValue(clickContext);
}
return context;
}
[[nodiscard]] std::shared_ptr<QVariant> ResolveClickHandlerContextRef(
const std::shared_ptr<QVariant> &current,
const OpenOptions &options) {
return options.clickHandlerContextRef
? options.clickHandlerContextRef
: (current ? current : std::make_shared<QVariant>());
}
void ProcessZoomShortcut(not_null<Delegate*> delegate, QKeyEvent *event) {
if (!(event->modifiers() & Qt::ControlModifier)) {
return;
}
if (event->key() == Qt::Key_Plus || event->key() == Qt::Key_Equal) {
event->accept();
delegate->ivSetZoom(delegate->ivZoom() + kZoomStep);
} else if (event->key() == Qt::Key_Minus) {
event->accept();
delegate->ivSetZoom(delegate->ivZoom() - kZoomStep);
} else if (event->key() == Qt::Key_0) {
event->accept();
delegate->ivSetZoom(0);
}
}
struct OpenTarget {
QString path;
QString fragment;
@@ -155,6 +239,10 @@ Controller::Controller(
, _title(std::move(title))
, _renderer(nullptr)
, _options(PrepareOpenOptions(std::move(options), delegate, _title)) {
_clickHandlerContextRef = ResolveClickHandlerContextRef(
_clickHandlerContextRef,
_options);
_options.clickHandlerContextRef = _clickHandlerContextRef;
createWindow();
}
@@ -169,6 +257,10 @@ Controller::Controller(
, _title(std::move(title))
, _renderer(renderer ? std::move(renderer) : std::make_shared<MathRenderer>())
, _options(PrepareOpenOptions(std::move(options), delegate, _title)) {
_clickHandlerContextRef = ResolveClickHandlerContextRef(
_clickHandlerContextRef,
_options);
_options.clickHandlerContextRef = _clickHandlerContextRef;
createWindow();
}
@@ -199,16 +291,68 @@ void Controller::update(
_preparedContent = std::move(content);
_title = std::move(title);
_options = PrepareOpenOptions(std::move(options), _delegate, _title);
if (_window) {
_window->setTitle(_title);
_window->setWindowTitle(_title);
_clickHandlerContextRef = ResolveClickHandlerContextRef(
_clickHandlerContextRef,
_options);
_options.clickHandlerContextRef = _clickHandlerContextRef;
if (_menu) {
_menu = nullptr;
_menuToggle->setForceRippled(false);
}
refreshTitle();
createPreview();
if (_window && _window->isActiveWindow() && _preview) {
_preview->setFocus();
}
}
void Controller::updateOptions(OpenOptions options) {
const auto initialFragment = options.initialFragment;
auto refreshed = PrepareOpenOptions(std::move(options), _delegate, _title);
if (refreshed.sourceName.isEmpty()) {
refreshed.sourceName = _options.sourceName;
}
if (refreshed.sourcePath.isEmpty()) {
refreshed.sourcePath = _options.sourcePath;
}
if (refreshed.sourceUrl.isEmpty()) {
refreshed.sourceUrl = _options.sourceUrl;
}
if (refreshed.viewerKind == ViewerKind::Auto) {
refreshed.viewerKind = _options.viewerKind;
}
if (!refreshed.openSource) {
refreshed.openSource = _options.openSource;
}
if (!refreshed.activateMedia) {
refreshed.activateMedia = _options.activateMedia;
}
_options = std::move(refreshed);
if (!_clickHandlerContextRef) {
_clickHandlerContextRef = std::make_shared<QVariant>();
}
_options.clickHandlerContextRef = _clickHandlerContextRef;
if (_clickHandlerContextRef) {
*_clickHandlerContextRef = ExtendClickHandlerContext(
_options.clickHandlerContext,
_show);
}
if (_menu) {
_menu = nullptr;
_menuToggle->setForceRippled(false);
}
refreshTitle();
if (!initialFragment.isEmpty() && _preview) {
const auto scrolled = ScrollMarkdownPreviewToAnchor(
_preview.get(),
initialFragment);
static_cast<void>(scrolled);
}
if (_window && _window->isActiveWindow() && _preview) {
_preview->setFocus();
}
}
bool Controller::active() const {
return _window && _window->isActiveWindow();
}
@@ -223,11 +367,122 @@ void Controller::close() {
_events.fire({ Event::Type::Close });
}
void Controller::createPreview() {
if (!_window) {
ViewerKind Controller::viewerKind() const {
return ResolveViewerKind(_options);
}
QString Controller::subtitleText() const {
return SubtitleText(_options, _title);
}
bool Controller::canOpenSource() const {
if (_options.openSource) {
return true;
}
return (viewerKind() == ViewerKind::InstantView)
? !_options.sourceUrl.isEmpty()
: !_options.sourcePath.isEmpty();
}
bool Controller::canShare() const {
return static_cast<bool>(_options.share);
}
void Controller::refreshTitle() {
if (_window) {
_window->setTitle(_title);
_window->setWindowTitle(_title);
}
if (_subtitle) {
_subtitle->setText(subtitleText());
updateTitleGeometry(_window->body()->width());
}
}
void Controller::updateTitleGeometry(int newWidth) const {
_subtitleWrap->setGeometry(0, 0, newWidth, st::ivSubtitleHeight);
_subtitle->resizeToWidth(newWidth
- st::ivSubtitleLeft
- _menuToggle->width());
_subtitle->moveToLeft(st::ivSubtitleLeft, st::ivSubtitleTop);
_menuToggle->moveToRight(0, 0);
if (_titleShadow) {
_titleShadow->resizeToWidth(newWidth);
_titleShadow->move(0, st::ivSubtitleHeight);
}
}
void Controller::openSource() {
if (_options.openSource) {
_options.openSource();
} else if (viewerKind() == ViewerKind::InstantView) {
File::OpenUrl(_options.sourceUrl);
} else {
File::Launch(_options.sourcePath);
}
}
void Controller::showMenu() {
if (!_window || _menu) {
return;
}
const auto parent = _window->body();
_menu = base::make_unique_q<Ui::PopupMenu>(
_window.get(),
st::popupMenuWithIcons);
_menu->setDestroyedCallback(crl::guard(_window.get(), [
this,
menu = _menu.get()] {
if (_menu == menu) {
_menuToggle->setForceRippled(false);
}
}));
_menuToggle->setForceRippled(true);
const auto action = _menu->addAction(
OpenSourceLabel(viewerKind()),
crl::guard(_window.get(), [=] {
openSource();
}),
OpenSourceIcon(viewerKind()));
action->setEnabled(canOpenSource());
if (canShare()) {
_menu->addAction(
tr::lng_iv_share(tr::now),
crl::guard(_window.get(), [=, share = _options.share] {
share(_show);
}),
&st::menuIconShare);
}
_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
_menu->popup(_window->body()->mapToGlobal(
QPoint(_window->body()->width(), 0) + st::ivMenuPosition));
}
void Controller::createLayerManager() {
if (!_window || _layerManager) {
return;
}
_layerManager = std::make_unique<Ui::LayerManager>(
not_null{ _window->body().get() });
_layerManager->setHideByBackgroundClick(false);
_show = _layerManager->uiShow();
}
void Controller::createPreview() {
if (!_container) {
return;
}
const auto parent = _container;
auto options = _options;
options.clickHandlerContextRef = _clickHandlerContextRef;
options.clickHandlerContext = ExtendClickHandlerContext(
std::move(options.clickHandlerContext),
_show);
if (options.clickHandlerContextRef) {
*options.clickHandlerContextRef = options.clickHandlerContext;
}
const auto callback = [=](Event event) {
_events.fire(std::move(event));
};
@@ -238,23 +493,48 @@ void Controller::createPreview() {
std::move(*_preparedContent),
_renderer,
callback,
_options)
options)
: CreateMarkdownPreviewWidget(
parent,
*_document,
callback,
_options);
options);
_preparedContent.reset();
_preview->setGeometry(parent->rect());
parent->sizeValue() | rpl::on_next([=](QSize size) {
_preview->resize(size);
}, _preview->lifetime());
if (_titleShadow) {
MarkdownPreviewScrollTopValue(
_preview.get()
) | rpl::on_next([=](int scrollTop) {
_titleShadow->toggle(
(scrollTop > 0),
anim::type::normal);
}, _preview->lifetime());
}
_preview->show();
}
void Controller::createWindow() {
_window = std::make_unique<Ui::RpWindow>();
const auto window = _window.get();
_subtitleWrap = std::make_unique<Ui::RpWidget>(window->body().get());
_subtitle = std::make_unique<Ui::FlatLabel>(
_subtitleWrap.get(),
subtitleText(),
st::ivSubtitle);
_subtitle->setSelectable(true);
_menuToggle.create(_subtitleWrap.get(), st::ivMenuToggle);
_menuToggle->setClickedCallback([=] {
showMenu();
});
_subtitleWrap->paintRequest() | rpl::on_next([=](QRect clip) {
QPainter(_subtitleWrap.get()).fillRect(clip, st::windowBg);
}, _subtitleWrap->lifetime());
window->body()->widthValue() | rpl::on_next([=](int width) {
updateTitleGeometry(width);
}, _subtitle->lifetime());
window->setTitle(_title);
window->setWindowTitle(_title);
window->setGeometry(_delegate->ivGeometry(window));
@@ -267,43 +547,58 @@ void Controller::createWindow() {
window->body()->paintRequest() | rpl::on_next([=](QRect clip) {
QPainter(window->body().get()).fillRect(clip, st::windowBg);
}, window->body()->lifetime());
_container = Ui::CreateChild<Ui::RpWidget>(window->body().get());
rpl::combine(
window->body()->sizeValue(),
_subtitleWrap->heightValue()
) | rpl::on_next([=](QSize size, int titleHeight) {
_container->setGeometry(QRect(QPoint(), size).marginsRemoved(
{ 0, titleHeight, 0, 0 }));
}, _container->lifetime());
_container->paintRequest() | rpl::on_next([=](QRect clip) {
QPainter(_container).fillRect(clip, st::windowBg);
}, _container->lifetime());
_titleShadow.create(window->body().get());
updateTitleGeometry(window->body()->width());
createLayerManager();
createPreview();
_container->show();
window->events() | rpl::on_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::Close) {
close();
} else if (e->type() == QEvent::KeyPress) {
const auto event = static_cast<QKeyEvent*>(e.get());
if (event->modifiers() & Qt::ControlModifier) {
if (event->key() == Qt::Key_Plus
|| event->key() == Qt::Key_Equal) {
event->accept();
_delegate->ivSetZoom(_delegate->ivZoom() + kZoomStep);
return;
} else if (event->key() == Qt::Key_Minus) {
event->accept();
_delegate->ivSetZoom(_delegate->ivZoom() - kZoomStep);
return;
} else if (event->key() == Qt::Key_0) {
event->accept();
_delegate->ivSetZoom(0);
return;
}
}
if (event->key() == Qt::Key_Escape) {
event->accept();
close();
}
}
}, window->lifetime());
base::install_event_filter(window, qApp, [=](not_null<QEvent*> e) {
if (e->type() != QEvent::ShortcutOverride || !window->isActiveWindow()) {
return base::EventFilterResult::Continue;
}
const auto event = static_cast<QKeyEvent*>(e.get());
if (event->matches(QKeySequence::Close)) {
close();
return base::EventFilterResult::Cancel;
}
const auto previousAccepted = event->isAccepted();
ProcessZoomShortcut(_delegate, event);
return event->isAccepted() && !previousAccepted
? base::EventFilterResult::Cancel
: base::EventFilterResult::Continue;
});
window->show();
}
std::unique_ptr<Controller> TryOpenLocalFile(
not_null<Delegate*> delegate,
const QString &path) {
const QString &path,
OpenOptions options) {
const auto &limits = ParseLimitsForIv();
const auto target = ParseOpenTarget(path);
@@ -360,9 +655,14 @@ std::unique_ptr<Controller> TryOpenLocalFile(
).arg(parsed - validated
).arg(target.path));
auto options = OpenOptions();
if (options.sourceName.isEmpty()) {
options.sourceName = source.name;
}
options.sourcePath = std::move(source.path);
options.initialFragment = std::move(target.fragment);
if (options.viewerKind == ViewerKind::Auto) {
options.viewerKind = ViewerKind::LocalFile;
}
return std::make_unique<Controller>(
delegate,
std::move(parseResult.document),
@@ -1,12 +1,23 @@
#pragma once
#include "base/object_ptr.h"
#include "base/unique_qptr.h"
#include "iv/markdown/iv_markdown_document.h"
#include "iv/markdown/iv_markdown_prepare.h"
#include "ui/widgets/rp_window.h"
#include <optional>
#include <QtCore/QString>
#include <QtCore/QVariant>
namespace Ui {
class FlatLabel;
class IconButton;
class LayerManager;
class PopupMenu;
class Show;
class FadeShadow;
} // namespace Ui
namespace Iv::Markdown {
@@ -30,6 +41,7 @@ public:
MarkdownArticleContent content,
QString title,
OpenOptions options = {});
void updateOptions(OpenOptions options = {});
[[nodiscard]] bool active() const;
void minimize();
@@ -43,7 +55,16 @@ public:
private:
void close();
void createWindow();
void createLayerManager();
void createPreview();
void updateTitleGeometry(int newWidth) const;
void showMenu();
void openSource();
[[nodiscard]] ViewerKind viewerKind() const;
[[nodiscard]] QString subtitleText() const;
[[nodiscard]] bool canOpenSource() const;
[[nodiscard]] bool canShare() const;
void refreshTitle();
const not_null<Delegate*> _delegate;
@@ -52,7 +73,16 @@ private:
QString _title;
const std::shared_ptr<MathRenderer> _renderer;
OpenOptions _options;
std::shared_ptr<QVariant> _clickHandlerContextRef;
std::unique_ptr<Ui::RpWindow> _window;
std::unique_ptr<Ui::RpWidget> _subtitleWrap;
std::unique_ptr<Ui::FlatLabel> _subtitle;
object_ptr<Ui::IconButton> _menuToggle = { nullptr };
object_ptr<Ui::FadeShadow> _titleShadow = { nullptr };
base::unique_qptr<Ui::PopupMenu> _menu;
Ui::RpWidget *_container = nullptr;
std::unique_ptr<Ui::LayerManager> _layerManager;
std::shared_ptr<Ui::Show> _show;
std::unique_ptr<Ui::RpWidget> _preview;
rpl::event_stream<Event> _events;
@@ -63,6 +93,7 @@ private:
[[nodiscard]] std::unique_ptr<Controller> TryOpenLocalFile(
not_null<Delegate*> delegate,
const QString &path);
const QString &path,
OpenOptions options = {});
} // namespace Iv::Markdown
@@ -37,6 +37,18 @@ constexpr auto kMaxVisualQuoteDepth = 3;
return std::max(int((numerator + denominator - 1) / denominator), 1);
}
[[nodiscard]] QString StripOneTrailingNewline(QString text) {
if (text.endsWith(u"\r\n"_q)) {
text.chop(2);
} else if (!text.isEmpty()) {
const auto last = text.back();
if ((last == QChar(u'\n')) || (last == QChar(u'\r'))) {
text.chop(1);
}
}
return text;
}
[[nodiscard]] int FlowFormulaTextSize(
PreparedBlockKind kind,
int headingLevel,
@@ -300,7 +312,7 @@ void AppendRichBlock(
[[nodiscard]] PreparedBlock PrepareCodeBlock(const MarkdownNode &node) {
auto block = PreparedBlock();
block.kind = PreparedBlockKind::CodeBlock;
block.text.text = node.text;
block.text.text = StripOneTrailingNewline(node.text);
block.codeLanguage = FirstInfoToken(node.info);
return block;
}
@@ -45,6 +45,18 @@ void SortPreparedIvRichText(PreparedIvRichText *text) {
SortEntities(&text->text);
}
[[nodiscard]] QString StripOneTrailingNewline(QString text) {
if (text.endsWith(u"\r\n"_q)) {
text.chop(2);
} else if (!text.isEmpty()) {
const auto last = text.back();
if ((last == QChar(u'\n')) || (last == QChar(u'\r'))) {
text.chop(1);
}
}
return text;
}
[[nodiscard]] PreparedBlock EmptyParagraphBlock() {
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Paragraph;
@@ -456,7 +468,7 @@ void SortPreparedIvRichText(PreparedIvRichText *text) {
block.kind = PreparedBlockKind::CodeBlock;
block.anchorId = std::move(anchorId);
block.codeLanguage = qs(data.vlanguage()).trimmed();
block.text.text = prepared.text.text;
block.text.text = StripOneTrailingNewline(prepared.text.text);
result->push_back(std::move(block));
return true;
}, [&](const MTPDpageBlockFooter &data) {
@@ -53,6 +53,12 @@ namespace {
: PrepareTerminalFailureName(failure.terminal);
}
[[nodiscard]] QVariant CurrentClickHandlerContext(const OpenOptions &options) {
return options.clickHandlerContextRef
? *options.clickHandlerContextRef
: options.clickHandlerContext;
}
} // namespace
class MarkdownPreviewRoot final : public Ui::RpWidget {
@@ -66,15 +72,16 @@ public:
QWidget *parent,
MarkdownArticleContent content,
std::shared_ptr<MathRenderer> renderer,
Fn<void(Event)> callback,
const OpenOptions &options);
Fn<void(Event)> callback,
const OpenOptions &options);
bool scrollToAnchor(const QString &anchorId);
[[nodiscard]] rpl::producer<int> scrollTopValue() const;
private:
void setup();
void prepareArticle();
void activateLink(const PreparedLink &link, Qt::MouseButton button);
void applyPreparedContent(MarkdownArticleContent prepared, int prepareMs);
[[nodiscard]] bool scrollToAnchor(const QString &anchorId);
void updateBodyVisibleTopBottom();
void updateChildrenGeometry(QSize size);
void updateFailureGeometry();
@@ -141,6 +148,9 @@ void MarkdownPreviewRoot::setup() {
_scroll->hide();
if (_body) {
_body->hide();
_body->setClickHandlerContext(
CurrentClickHandlerContext(_options),
_options.clickHandlerContextRef);
_body->setLinkActivationCallback([=](
const PreparedLink &link,
Qt::MouseButton button) {
@@ -234,7 +244,9 @@ void MarkdownPreviewRoot::activateLink(
}
switch (link.kind) {
case PreparedLinkKind::External:
HiddenUrlClickHandler::Open(link.target);
HiddenUrlClickHandler::Open(
link.target,
CurrentClickHandlerContext(_options));
break;
case PreparedLinkKind::Anchor:
case PreparedLinkKind::Footnote:
@@ -252,6 +264,7 @@ void MarkdownPreviewRoot::activateLink(
_callback({
.type = Event::Type::OpenFile,
.url = std::move(target),
.context = CurrentClickHandlerContext(_options),
});
} break;
case PreparedLinkKind::RejectedRelative:
@@ -332,6 +345,24 @@ bool MarkdownPreviewRoot::scrollToAnchor(const QString &anchorId) {
return true;
}
rpl::producer<int> MarkdownPreviewRoot::scrollTopValue() const {
return _scroll
? _scroll->scrollTopValue()
: rpl::single(0);
}
bool ScrollMarkdownPreviewToAnchor(
Ui::RpWidget *preview,
const QString &anchorId) {
const auto root = dynamic_cast<MarkdownPreviewRoot*>(preview);
return root ? root->scrollToAnchor(anchorId) : false;
}
rpl::producer<int> MarkdownPreviewScrollTopValue(Ui::RpWidget *preview) {
const auto root = dynamic_cast<MarkdownPreviewRoot*>(preview);
return root ? root->scrollTopValue() : rpl::single(0);
}
void MarkdownPreviewRoot::updateBodyVisibleTopBottom() {
if (_body) {
const auto scrollTop = _scroll->scrollTop();
@@ -22,5 +22,10 @@ namespace Iv::Markdown {
std::shared_ptr<MathRenderer> renderer,
Fn<void(Event)> callback,
const OpenOptions &options = {});
bool ScrollMarkdownPreviewToAnchor(
Ui::RpWidget *preview,
const QString &anchorId);
[[nodiscard]] rpl::producer<int> MarkdownPreviewScrollTopValue(
Ui::RpWidget *preview);
} // namespace Iv::Markdown
@@ -1,5 +1,9 @@
#include "iv/markdown/iv_markdown_view_widget.h"
#include "base/weak_ptr.h"
#include "core/credits_amount.h"
#include "core/click_handler_types.h"
#include <QtCore/QElapsedTimer>
#include <QtGui/QClipboard>
#include <QtGui/QContextMenuEvent>
@@ -10,9 +14,9 @@
#include <QtGui/QMouseEvent>
#include <QtWidgets/QApplication>
#include "base/weak_ptr.h"
#include "core/file_utilities.h"
#include "lang/lang_keys.h"
#include "ui/layers/show.h"
#include "ui/chat/chat_style.h"
#include "ui/integration.h"
#include "ui/widgets/popup_menu.h"
@@ -114,6 +118,13 @@ void MarkdownDocumentWidget::setMediaActivationCallback(
_activateMedia = std::move(callback);
}
void MarkdownDocumentWidget::setClickHandlerContext(
QVariant context,
std::shared_ptr<QVariant> contextRef) {
_clickHandlerContext = std::move(context);
_clickHandlerContextRef = std::move(contextRef);
}
void MarkdownDocumentWidget::setArticle(
std::shared_ptr<MarkdownArticle> article) {
ClickHandler::clearActive(this);
@@ -275,8 +286,9 @@ void MarkdownDocumentWidget::contextMenuEvent(QContextMenuEvent *e) {
} else if (!contextText.empty()) {
_contextMenu->addAction(
tr::lng_context_copy_text(tr::now),
[text = contextText] {
[text = contextText, this] {
TextUtilities::SetClipboardText(text);
showToast(tr::lng_text_copied(tr::now));
},
&st::menuIconCopy);
}
@@ -549,9 +561,23 @@ TextForMimeData MarkdownDocumentWidget::getSelectedText() const {
: TextForMimeData();
}
QVariant MarkdownDocumentWidget::clickHandlerContext() const {
return _clickHandlerContextRef
? *_clickHandlerContextRef
: _clickHandlerContext;
}
void MarkdownDocumentWidget::showToast(const QString &text) const {
const auto context = clickHandlerContext().value<ClickHandlerContext>();
if (context.show) {
context.show->showToast(text);
}
}
void MarkdownDocumentWidget::copySelectedText() {
if (const auto text = getSelectedText(); !text.empty()) {
TextUtilities::SetClipboardText(text);
showToast(tr::lng_text_copied(tr::now));
}
}
@@ -765,7 +791,31 @@ MarkdownArticleHitTestResult MarkdownDocumentWidget::dragActionFinish(
if (state.preparedLink && _activateLink) {
_activateLink(*state.preparedLink, button);
} else {
ActivateClickHandler(window(), activated, button);
auto clickHandlerContext = this->clickHandlerContext();
if (std::dynamic_pointer_cast<MonospaceClickHandler>(activated)) {
const auto context = clickHandlerContext.value<ClickHandlerContext>();
if (context.show) {
auto sanitized = ClickHandlerContext();
sanitized.itemId = context.itemId;
sanitized.elementDelegate = context.elementDelegate;
sanitized.botWebviewContext = context.botWebviewContext;
sanitized.show = context.show;
sanitized.mayShowConfirmation = context.mayShowConfirmation;
sanitized.skipBotAutoLogin = context.skipBotAutoLogin;
sanitized.botStartAutoSubmit = context.botStartAutoSubmit;
sanitized.ignoreIv = context.ignoreIv;
sanitized.dark = context.dark;
sanitized.peer = context.peer;
clickHandlerContext = QVariant::fromValue(sanitized);
const auto handled = Ui::Integration::Instance().copyPreOnClick(
clickHandlerContext);
static_cast<void>(handled);
}
}
auto context = ClickContext();
context.button = button;
context.other = std::move(clickHandlerContext);
ActivateClickHandler(window(), activated, context);
}
} else if ((button == Qt::LeftButton || button == Qt::MiddleButton)
&& state.mediaActivation.kind != MediaActivationKind::None
@@ -10,6 +10,7 @@
#include <functional>
#include <memory>
#include <QtCore/QVariant>
namespace Ui {
class PopupMenu;
@@ -31,6 +32,9 @@ public:
std::function<void(const PreparedLink &, Qt::MouseButton)> callback);
void setMediaActivationCallback(
std::function<bool(const MediaActivation &, Qt::MouseButton)> callback);
void setClickHandlerContext(
QVariant context,
std::shared_ptr<QVariant> contextRef = nullptr);
void setArticle(std::shared_ptr<MarkdownArticle> article);
void setZoom(int value);
void refreshPalette();
@@ -77,6 +81,8 @@ private:
[[nodiscard]] MarkdownArticleSelection selectionFromHit(
const MarkdownArticleHitTestResult &result) const;
[[nodiscard]] TextForMimeData getSelectedText() const;
[[nodiscard]] QVariant clickHandlerContext() const;
void showToast(const QString &text) const;
void copySelectedText();
void syncArticleVisibleTopBottom();
@@ -102,6 +108,8 @@ private:
std::unique_ptr<Ui::Text::QuotePaintCache> _blockquotePaintCache;
std::function<void(const PreparedLink &, Qt::MouseButton)> _activateLink;
std::function<bool(const MediaActivation &, Qt::MouseButton)> _activateMedia;
QVariant _clickHandlerContext;
std::shared_ptr<QVariant> _clickHandlerContextRef;
MarkdownArticleSelection _selection;
MarkdownArticleSelection _savedSelection;
MarkdownArticleSelectionEndpoints _selectionEndpoints;
@@ -1382,6 +1382,18 @@ void ForEachPreparedLink(
});
}
[[nodiscard]] std::vector<const PreparedBlock*> CollectPreparedBlocksByKind(
const std::vector<PreparedBlock> &blocks,
PreparedBlockKind kind) {
auto result = std::vector<const PreparedBlock*>();
ForEachPreparedBlock(blocks, [&](const PreparedBlock &block) {
if (block.kind == kind) {
result.push_back(&block);
}
});
return result;
}
struct InlineTextObjectMatch {
EntityInText entity;
InlineTextObjectEntity object;
@@ -2263,6 +2275,96 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
}
}
void CheckCodeBlockTrailingNewlineTrim(bool *ok) {
const auto markdownLabel = u"generated-code-block-trailing-newline"_q;
const auto parsed = ParseMarkdownForIv(QByteArray(R"(```cpp
alpha
```
beta
)"), ParseOptions{ markdownLabel });
Check(
parsed.ok,
markdownLabel + u" parse failed: "_q + parsed.error,
ok);
if (!parsed.ok) {
return;
}
auto renderer = std::make_shared<MathRenderer>();
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[1]->text.text == u"beta"_q,
markdownLabel + u" indented block trims one newline"_q,
ok);
}
const auto nativeLabel = u"native-iv-preformatted-trailing-newline"_q;
auto nativeBlocks = QVector<MTPPageBlock>();
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[1]->text.text == u"double\n"_q,
nativeLabel + u" extra trailing newline preserved"_q,
ok);
}
}
void CheckNativeInstantViewArticleCoverage(bool *ok) {
const auto placeholderLabel = u"native-iv-placeholder-article"_q;
auto placeholderBlocks = QVector<MTPPageBlock>();
@@ -4208,6 +4310,7 @@ ThisIsALongUnbrokenStringToTestWrappingBehavior_ABCD1234EFGH5678IJKL
CheckInlineTextObjectPrepareCoverage(&ok);
CheckNativeInstantViewPrepareCoverage(&ok);
CheckCodeBlockTrailingNewlineTrim(&ok);
CheckPrepareCoverage(markdownFixture, latexFixture, &ok);
CheckPrepareLinkClassification(markdownFixture.path, &ok);
CheckArticleRenderSmoke(markdownFixture, latexFixture, &ok);