From 5eed2bf42d81636bc232f4f0adaa31c045c607a1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 1 May 2026 20:08:03 +0700 Subject: [PATCH] Open markdown through Core::App().iv(). --- .../data/data_document_resolver.cpp | 10 +- Telegram/SourceFiles/iv/iv_instance.cpp | 39 +++ Telegram/SourceFiles/iv/iv_instance.h | 12 + .../iv/markdown/iv_markdown_common.h | 10 + .../iv/markdown/iv_markdown_controller.cpp | 312 ++++++------------ .../iv/markdown/iv_markdown_controller.h | 47 ++- .../iv/markdown/iv_markdown_view.cpp | 43 ++- .../iv/markdown/iv_markdown_view.h | 2 + 8 files changed, 248 insertions(+), 227 deletions(-) diff --git a/Telegram/SourceFiles/data/data_document_resolver.cpp b/Telegram/SourceFiles/data/data_document_resolver.cpp index de4d90b669..774c12a6ea 100644 --- a/Telegram/SourceFiles/data/data_document_resolver.cpp +++ b/Telegram/SourceFiles/data/data_document_resolver.cpp @@ -20,8 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_gif.h" #include "history/history.h" #include "history/history_item.h" -#include "iv/markdown/iv_markdown_common.h" -#include "iv/markdown/iv_markdown_controller.h" +#include "iv/iv_instance.h" #include "lang/lang_keys.h" #include "media/player/media_player_instance.h" #include "platform/platform_file_utilities.h" @@ -252,12 +251,9 @@ void ResolveDocument( if (!openImageInApp()) { const auto path = document->filepath(true); if (!path.isEmpty()) { - const auto fileName = QFileInfo(path).fileName(); - if (Iv::Markdown::LooksLikeMarkdownFile(fileName) - && Iv::Markdown::TryOpenLocalFile(path)) { - return; + if (!Core::App().iv().showMarkdown(path)) { + LaunchWithWarning(path, item); } - LaunchWithWarning(path, item); } else if (document->status == FileReady || document->status == FileDownloadFailed) { DocumentSaveClickHandler::Save( diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp index dd7653b79c..a30abdc396 100644 --- a/Telegram/SourceFiles/iv/iv_instance.cpp +++ b/Telegram/SourceFiles/iv/iv_instance.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "history/history_item_helpers.h" #include "info/profile/info_profile_values.h" +#include "iv/markdown/iv_markdown_controller.h" #include "iv/iv_controller.h" #include "iv/iv_data.h" #include "lang/lang_keys.h" @@ -204,6 +205,10 @@ private: }; +struct MarkdownShown { + std::unique_ptr controller; +}; + Shown::Shown( not_null delegate, not_null session, @@ -1132,6 +1137,40 @@ void Instance::showTonSite( }, _tonSite->lifetime()); } +bool Instance::showMarkdown( + const QString &path, + QVariant context) { + auto i = _markdowns.find(path); + if (i == end(_markdowns)) { + if (auto controller = Markdown::TryOpenLocalFile(_delegate, path)) { + controller->events() | rpl::on_next([=](Markdown::Event event) { + using Type = Markdown::Event::Type; + switch (event.type) { + case Type::Close: + _markdowns.take(path); + break; + case Type::Quit: + Shortcuts::Launch(Shortcuts::Command::Quit); + break; + case Type::OpenFile: + if (!showMarkdown(event.url)) { + DEBUG_LOG(("Native Markdown IV: " + "failed local markdown link: %1" + ).arg(event.url)); + } + break; + } + }, controller->lifetime()); + + i = _markdowns.emplace(path, std::move(controller)).first; + } else { + return false; + } + } + i->second->activate(); + return true; +} + void Instance::requestFull( not_null session, const QString &id) { diff --git a/Telegram/SourceFiles/iv/iv_instance.h b/Telegram/SourceFiles/iv/iv_instance.h index c48cf569ee..1b7bd63d91 100644 --- a/Telegram/SourceFiles/iv/iv_instance.h +++ b/Telegram/SourceFiles/iv/iv_instance.h @@ -18,6 +18,10 @@ namespace Window { class SessionController; } // namespace Window +namespace Iv::Markdown { +class Controller; +} // namespace Iv::Markdown + namespace Iv { class Data; @@ -55,6 +59,10 @@ public: const QString &uri, QVariant context = {}); + bool showMarkdown( + const QString &path, + QVariant context = {}); + [[nodiscard]] bool hasActiveWindow( not_null session) const; @@ -104,6 +112,10 @@ private: std::unique_ptr _tonSite; + base::flat_map< + QString, + std::unique_ptr> _markdowns; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_common.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_common.h index 433b7c4945..b1c40f0d8b 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_common.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_common.h @@ -23,4 +23,14 @@ struct ParseOptions { const QString &fileName, const QString &mimeType = QString()); +struct Event { + enum class Type { + Close, + Quit, + OpenFile, + }; + Type type = Type::Close; + QString url; +}; + } // namespace Iv::Markdown diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.cpp index ff8f1d52b7..89e6a3b21c 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.cpp @@ -34,23 +34,42 @@ struct OpenTarget { return info.exists() && info.isFile() && info.isReadable(); } -[[nodiscard]] bool ReadLocalSource(const QString &path, QByteArray *bytes) { +struct ReadSource { + QString path; + QString name; + QByteArray bytes; + + explicit operator bool() const { + return !path.isEmpty(); + } +}; +[[nodiscard]] ReadSource ReadLocalSource( + const QString &path, + const MarkdownParseLimits &limits) { const auto info = QFileInfo(path); - if (!IsReadableLocalFile(info)) { - return false; + auto name = info.fileName(); + if (!IsReadableLocalFile(info) || !LooksLikeMarkdownFile(name)) { + return {}; + } else if (info.size() > limits.maxSourceBytes) { + DEBUG_LOG(("Native Markdown IV: rejected local file too large: %1" + ).arg(path)); + return {}; } auto file = QFile(path); if (!file.open(QIODevice::ReadOnly)) { - return false; + return {}; } - const auto data = file.readAll(); - if (file.error() != QFileDevice::NoError) { - return false; + auto data = file.read(limits.maxSourceBytes); + if (file.error() != QFileDevice::NoError || !file.atEnd()) { + DEBUG_LOG(("Native Markdown IV: could not read local file: %1" + ).arg(path)); + return {}; } - if (bytes) { - *bytes = data; - } - return true; + return { + .path = info.absoluteFilePath(), + .name = std::move(name), + .bytes = std::move(data), + }; } [[nodiscard]] QString NormalizeFragmentId(QString fragment) { @@ -113,77 +132,28 @@ void LogDocumentWarnings( } } -class Controller final { -public: - Controller( - PreparedDocument document, - QString title, - QString sourcePath, - QString initialFragment); - - void activate(); - -private: - void close(); - void createWindow(); - void finishClose(); - - PreparedDocument _document; - QString _title; - QString _sourcePath; - QString _initialFragment; - Iv::DelegateImpl _delegate; - std::unique_ptr _window; - std::unique_ptr _preview; - bool _closing = false; - -}; - -[[nodiscard]] auto &ActiveControllers() { - static auto controllers = std::vector>(); - return controllers; -} - -void RemoveController(Controller *controller) { - auto &active = ActiveControllers(); - const auto i = std::find_if( - active.begin(), - active.end(), - [=](const std::unique_ptr &value) { - return value.get() == controller; - }); - if (i != active.end()) { - active.erase(i); - } -} - -void OpenDocumentWindow( - PreparedDocument document, - QString title, - QString sourcePath, - QString initialFragment) { - auto controller = std::make_unique( - std::move(document), - std::move(title), - std::move(sourcePath), - std::move(initialFragment)); - const auto raw = controller.get(); - ActiveControllers().push_back(std::move(controller)); - raw->activate(); -} +} // namespace Controller::Controller( - PreparedDocument document, - QString title, - QString sourcePath, - QString initialFragment) -: _document(std::move(document)) + not_null delegate, + PreparedDocument document, + QString title, + QString sourcePath, + QString initialFragment) +: _delegate(delegate) +, _document(std::move(document)) , _title(std::move(title)) , _sourcePath(std::move(sourcePath)) , _initialFragment(std::move(initialFragment)) { createWindow(); } +Controller::~Controller() = default; + +rpl::lifetime &Controller::lifetime() { + return _lifetime; +} + void Controller::activate() { if (_window->isMinimized()) { _window->showNormal(); @@ -199,9 +169,7 @@ void Controller::activate() { } void Controller::close() { - if (_window) { - _window->close(); - } + _events.fire({ Event::Type::Close }); } void Controller::createWindow() { @@ -209,50 +177,51 @@ void Controller::createWindow() { const auto window = _window.get(); window->setTitle(_title); window->setWindowTitle(_title); - window->setGeometry(_delegate.ivGeometry(window)); + window->setGeometry(_delegate->ivGeometry(window)); window->setMinimumSize({ st::windowMinWidth, st::windowMinHeight }); window->geometryValue( ) | rpl::distinct_until_changed( ) | rpl::skip(1) | rpl::on_next([=] { - _delegate.ivSaveGeometry(window); + _delegate->ivSaveGeometry(window); }, window->lifetime()); window->body()->paintRequest() | rpl::on_next([=](QRect clip) { QPainter(window->body().get()).fillRect(clip, st::windowBg); }, window->body()->lifetime()); - _preview = CreateMarkdownPreviewWidget( - _document, - OpenOptions{ - .sourceName = _title, - .sourcePath = _sourcePath, - .initialFragment = _initialFragment, - .delegate = &_delegate, - }); - _preview->setParent(window->body().get()); - _preview->setGeometry(QRect(QPoint(), window->body()->size())); - window->body()->sizeValue() | rpl::on_next([=](QSize size) { - _preview->setGeometry(QRect(QPoint(), size)); + const auto parent = window->body(); + const auto callback = [=](Event event) { + _events.fire(std::move(event)); + }; + _preview = CreateMarkdownPreviewWidget(parent, _document, callback, { + .sourceName = _title, + .sourcePath = _sourcePath, + .initialFragment = _initialFragment, + .delegate = _delegate, + }); + _preview->setGeometry(parent->rect()); + parent->sizeValue() | rpl::on_next([=](QSize size) { + _preview->resize(size); }, _preview->lifetime()); _preview->show(); window->events() | rpl::on_next([=](not_null e) { if (e->type() == QEvent::Close) { - finishClose(); + close(); } else if (e->type() == QEvent::KeyPress) { const auto event = static_cast(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); + _delegate->ivSetZoom(_delegate->ivZoom() + kZoomStep); return; } else if (event->key() == Qt::Key_Minus) { event->accept(); - _delegate.ivSetZoom(_delegate.ivZoom() - kZoomStep); + _delegate->ivSetZoom(_delegate->ivZoom() - kZoomStep); return; } else if (event->key() == Qt::Key_0) { event->accept(); - _delegate.ivSetZoom(0); + _delegate->ivSetZoom(0); return; } } @@ -266,136 +235,73 @@ void Controller::createWindow() { window->show(); } -void Controller::finishClose() { - if (_closing) { - return; - } - _closing = true; - crl::on_main([controller = this] { - RemoveController(controller); - }); -} - -} // namespace - -bool TryOpenLocalFile(const QString &path) { +std::unique_ptr TryOpenLocalFile( + not_null delegate, + const QString &path) { const auto &limits = ParseLimitsForIv(); const auto target = ParseOpenTarget(path); - const auto info = QFileInfo(target.path); - if (!IsReadableLocalFile(info)) { - return false; - } - if (info.size() > limits.maxSourceBytes) { - DEBUG_LOG(("Native Markdown IV: rejected local file too large: %1" - ).arg(target.path)); - return false; - } - auto bytes = QByteArray(); - if (!ReadLocalSource(target.path, &bytes)) { - return false; - } - if (bytes.size() > limits.maxSourceBytes) { - DEBUG_LOG(("Native Markdown IV: rejected local file too large: %1" - ).arg(target.path)); - return false; + const auto source = ReadLocalSource(target.path, limits); + if (!source) { + return nullptr; } + const auto &bytes = source.bytes; + const auto &fallbackTitle = source.name; - const auto fallbackTitle = info.fileName(); -#ifndef NDEBUG - auto validationTimer = QElapsedTimer(); - validationTimer.start(); -#endif - auto validated = ValidateMarkdownSourceForIv( + const auto start = crl::now(); + auto validateResult = ValidateMarkdownSourceForIv( bytes, ParseOptions{ fallbackTitle }); -#ifndef NDEBUG - const auto validationMs = validationTimer.elapsed(); -#endif - if (!validated.ok) { -#ifndef NDEBUG - DEBUG_LOG(("Native Markdown IV: source validation failure (%1, %2 ms): %3" - ).arg(validated.error - ).arg(validationMs + const auto validated = crl::now(); + if (!validateResult.ok) { + DEBUG_LOG(("Native Markdown IV: " + "source validation failure (%1, %2 ms): %3" + ).arg(validateResult.error + ).arg(validated - start ).arg(target.path)); -#else - DEBUG_LOG(("Native Markdown IV: source validation failure (%1): %2" - ).arg(validated.error - ).arg(target.path)); -#endif - return false; + return nullptr; } -#ifndef NDEBUG - auto parseTimer = QElapsedTimer(); - parseTimer.start(); -#endif - auto result = ParseMarkdownForIv(std::move(validated.source)); -#ifndef NDEBUG - const auto parseMs = parseTimer.elapsed(); -#endif - if (!result.ok) { - const auto &error = result.error; + auto parseResult = ParseMarkdownForIv(std::move(validateResult.source)); + const auto parsed = crl::now(); + if (!parseResult.ok) { + const auto &error = parseResult.error; if (error.startsWith(u"cmark-"_q)) { -#ifndef NDEBUG - DEBUG_LOG(("Native Markdown IV: cmark parse failure (%1, %2 ms): %3" + DEBUG_LOG(("Native Markdown IV: " + "cmark parse failure (%1, %2 ms): %3" ).arg(error - ).arg(parseMs + ).arg(parsed - validated ).arg(target.path)); -#else - DEBUG_LOG(("Native Markdown IV: cmark parse failure (%1): %2" - ).arg(error - ).arg(target.path)); -#endif } else { -#ifndef NDEBUG DEBUG_LOG(("Native Markdown IV: parse failure (%1, %2 ms): %3" ).arg(error - ).arg(parseMs + ).arg(parsed - validated ).arg(target.path)); -#else - DEBUG_LOG(("Native Markdown IV: parse failure (%1): %2" - ).arg(error - ).arg(target.path)); -#endif } - return false; - } -#ifndef NDEBUG - auto previewEligibilityTimer = QElapsedTimer(); - previewEligibilityTimer.start(); -#endif - if (!AcceptsPreview(result.document)) { -#ifndef NDEBUG - DEBUG_LOG(("Native Markdown IV: unsupported or empty document (%1 ms): %2" - ).arg(previewEligibilityTimer.elapsed() + return nullptr; + } else if (!AcceptsPreview(parseResult.document)) { + DEBUG_LOG(("Native Markdown IV: " + "unsupported or empty document (%1 ms): %2" + ).arg(crl::now() - parsed ).arg(target.path)); -#else - DEBUG_LOG(("Native Markdown IV: unsupported or empty document: %1" - ).arg(target.path)); -#endif - return false; + return nullptr; } - LogDocumentWarnings(result.document, target.path); + LogDocumentWarnings(parseResult.document, target.path); - const auto title = result.document.title.trimmed().isEmpty() - ? fallbackTitle - : result.document.title.trimmed(); - OpenDocumentWindow( - std::move(result.document), - title, - info.absoluteFilePath(), - target.fragment); -#ifndef NDEBUG - DEBUG_LOG(("Native Markdown IV: opened as native Markdown IV (%1 ms validate, %2 ms parse): %3" - ).arg(validationMs - ).arg(parseMs + DEBUG_LOG(("Native Markdown IV: " + "opening as native Markdown IV (%1 ms validate, %2 ms parse): %3" + ).arg(validated - start + ).arg(parsed - validated ).arg(target.path)); -#else - DEBUG_LOG(("Native Markdown IV: opened as native Markdown IV: %1" - ).arg(target.path)); -#endif - return true; + + return std::make_unique( + delegate, + std::move(parseResult.document), + (parseResult.document.title.trimmed().isEmpty() + ? fallbackTitle + : parseResult.document.title.trimmed()), + std::move(source.path), + std::move(target.fragment)); } } // namespace Iv::Markdown diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.h index d25420c231..abbbdf8bdf 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.h @@ -1,9 +1,54 @@ #pragma once +#include "iv/markdown/iv_markdown_document.h" +#include "ui/widgets/rp_window.h" + #include namespace Iv::Markdown { -[[nodiscard]] bool TryOpenLocalFile(const QString &path); +class Controller final { +public: + Controller( + not_null delegate, + PreparedDocument document, + QString title, + QString sourcePath, + QString initialFragment); + ~Controller(); + + void activate(); + + [[nodiscard]] bool active() const; + void minimize(); + + [[nodiscard]] rpl::producer events() const { + return _events.events(); + } + + [[nodiscard]] rpl::lifetime &lifetime(); + +private: + void close(); + void createWindow(); + + const not_null _delegate; + + PreparedDocument _document; + QString _title; + QString _sourcePath; + QString _initialFragment; + std::unique_ptr _window; + std::unique_ptr _preview; + + rpl::event_stream _events; + + rpl::lifetime _lifetime; + +}; + +[[nodiscard]] std::unique_ptr TryOpenLocalFile( + not_null delegate, + const QString &path); } // namespace Iv::Markdown diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_view.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_view.cpp index d0427783bf..2b46633717 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_view.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_view.cpp @@ -3227,9 +3227,10 @@ constexpr auto kDeferredPreparationConvertedNodes = 1200; class MarkdownPreviewRoot final : public Ui::RpWidget { public: MarkdownPreviewRoot( + QWidget *parent, const PreparedDocument &document, - const OpenOptions &options, - QWidget *parent = nullptr); + Fn callback, + const OpenOptions &options); ~MarkdownPreviewRoot(); private: @@ -3254,6 +3255,7 @@ private: const OpenOptions _options; const std::shared_ptr _document; + const Fn _callback; Ui::ScrollArea *_scroll = nullptr; MarkdownDocumentWidget *_body = nullptr; Ui::FlatLabel *_loading = nullptr; @@ -3271,12 +3273,14 @@ private: }; MarkdownPreviewRoot::MarkdownPreviewRoot( + QWidget *parent, const PreparedDocument &document, - const OpenOptions &options, - QWidget *parent) + Fn callback, + const OpenOptions &options) : Ui::RpWidget(parent) , _options(options) , _document(std::make_shared(document)) +, _callback(std::move(callback)) , _renderer(std::make_shared()) , _pendingFragment(options.initialFragment) , _cancelled(std::make_shared(false)) { @@ -3448,17 +3452,18 @@ void MarkdownPreviewRoot::activateLink( } break; case PreparedLinkKind::LocalFile: { - auto path = link.target; - if (!link.fragment.isEmpty()) { - path += u"#"_q + link.fragment; - } - if (!TryOpenLocalFile(path)) { - DEBUG_LOG(("Native Markdown IV: failed local markdown link: %1").arg( - path)); - } + // Don't try opening local files from downloaded. + // The downloaded files should not known any local files. + // + //auto path = link.target; + //if (!link.fragment.isEmpty()) { + // path += u"#"_q + link.fragment; + //} + //_callback({ .type = Event::Type::OpenFile, .url = path }); } break; case PreparedLinkKind::RejectedRelative: - DEBUG_LOG(("Native Markdown IV: rejected relative markdown link: %1").arg( + DEBUG_LOG(("Native Markdown IV: " + "rejected relative markdown link: %1").arg( link.target)); break; case PreparedLinkKind::ToggleDetails: @@ -3613,9 +3618,15 @@ void MarkdownPreviewRoot::cancelInFlightRequest() { } // namespace std::unique_ptr CreateMarkdownPreviewWidget( - const PreparedDocument &document, - const OpenOptions &options) { - return std::make_unique(document, options); + QWidget *parent, + const PreparedDocument &document, + Fn callback, + const OpenOptions &options) { + return std::make_unique( + parent, + document, + std::move(callback), + options); } } // namespace Iv::Markdown diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_view.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_view.h index a4811ad2cd..7769016be6 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_view.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_view.h @@ -11,7 +11,9 @@ class RpWidget; namespace Iv::Markdown { [[nodiscard]] std::unique_ptr CreateMarkdownPreviewWidget( + QWidget *parent, const PreparedDocument &document, + Fn callback, const OpenOptions &options = {}); } // namespace Iv::Markdown