diff --git a/chrome/app/chrome_command_ids.h b/chrome/app/chrome_command_ids.h index 9097e34ba86031..2ea5c36ac48935 100644 --- a/chrome/app/chrome_command_ids.h +++ b/chrome/app/chrome_command_ids.h @@ -269,6 +269,7 @@ #define IDC_SHOW_GOOGLE_LENS_SHORTCUT 40282 #define IDC_SHOW_CUSTOMIZE_CHROME_SIDE_PANEL 40283 #define IDC_SHOW_CUSTOMIZE_CHROME_TOOLBAR 40284 +#define IDC_SHOW_AI_CHAT 40999 // Spell-check // Insert any additional suggestions before _LAST; these have to be consecutive. diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd index ef588a13b86630..dbe879afc9e587 100644 --- a/chrome/app/generated_resources.grd +++ b/chrome/app/generated_resources.grd @@ -8965,6 +8965,81 @@ Keep your key file in a safe place. You will need it to create new versions of y + + + + Chat + + + + + Chat + + + + Summarize this page + + + Explain it in simple language + + + Translate to... + + + Afrikaans, Albanian, Amharic, Arabic, Armenian, Assamese, Azerbaijani, Basque, Belarusian, Bengali, Bosnian, Bulgarian, Catalan, Cebuano, Chichewa, Chinese (Simplified), Chinese (Traditional), Corsican, Croatian, Czech, Danish, Dutch, English, Esperanto, Estonian, Filipino, Finnish, French, Frisian, Galician, Georgian, German, Greek, Gujarati, Haitian Creole, Hausa, Hawaiian, Hebrew, Hindi, Hmong, Hungarian, Icelandic, Igbo, Indonesian, Irish, Italian, Japanese, Javanese, Kannada, Kazakh, Khmer, Kinyarwanda, Korean, Kurdish (Kurmanji), Kyrgyz, Lao, Latin, Latvian, Lithuanian, Luxembourgish, Macedonian, Malagasy, Malay, Malayalam, Maltese, Maori, Marathi, Mongolian, Myanmar (Burmese), Nepali, Norwegian, Odia, Pashto, Persian, Polish, Portuguese, Punjabi, Romanian, Russian, Samoan, Scots Gaelic, Serbian, Sesotho, Shona, Sindhi, Sinhala, Slovak, Slovenian, Somali, Spanish, Sundanese, Swahili, Swedish, Tajik, Tamil, Tatar, Telugu, Thai, Turkish, Turkmen, Ukrainian, Urdu, Uyghur, Uzbek, Vietnamese, Welsh, Xhosa, Yiddish, Yoruba, Zulu + + + Draft a post for... + + + X (Twitter), Facebook, Instagram, LinkedIn + + + Fact check + + + Utilize the content of the active web page as context to provide concise, socially unbiased, and positive responses to your questions. + If a question is unrelated to the content or cannot be answered based on it, kindly let the user know that their query is beyond the scope of the content of the active web page, and offer a brief response informed by your broader knowledge. + Avoid using phrases like "the user's question" or "The user's question" in your answer. Instead, use "your question" or other suitable pronouns in your response. + Refrain from repeating or quoting the question verbatim. + For greetings such as "hi," "hello," or "hey," respond warmly, briefly referencing the content of the active web page, and politely suggest how you can assist further. + Here is the user's question: $1 + Here is the content of the active web page you should use as context: $2 + + + Provide a brief summary of the key takeaways for this page. + + + Explain this page in simple language. + + + Translate this page to + + + Draft a post about this page for + + + Fact check this page and highlight major claims that may require verification or be biased. + + + Ask anything... + + + Chat about this page + + + Your message is too long + + + An error has occurred. Please try again, restart the chat, or close and reopen the chat panel. + + + New Chat + + + Close + + Side Panel Resize Handle diff --git a/chrome/app/vector_icons/BUILD.gn b/chrome/app/vector_icons/BUILD.gn index 22370837a74523..837e19475e1ae9 100644 --- a/chrome/app/vector_icons/BUILD.gn +++ b/chrome/app/vector_icons/BUILD.gn @@ -40,6 +40,7 @@ aggregate_vector_icons("chrome_vector_icons") { "browser_tools_update.icon", "browser_tools_update_chrome_refresh.icon", "cast_chrome_refresh.icon", + "chat.icon", "chevron_right.icon", "chevron_right_chrome_refresh.icon", "chromium_minimize.icon", @@ -119,6 +120,7 @@ aggregate_vector_icons("chrome_vector_icons") { "menu_book_chrome_refresh.icon", "mixed_content.icon", "more_tools_menu.icon", + "more_horiz.icon", "move_group_to_new_window_refresh.icon", "my_location.icon", "name_window.icon", diff --git a/chrome/app/vector_icons/chat.icon b/chrome/app/vector_icons/chat.icon new file mode 100644 index 00000000000000..0475a1d4db2918 --- /dev/null +++ b/chrome/app/vector_icons/chat.icon @@ -0,0 +1,13 @@ +CANVAS_DIMENSIONS, 18, +FILL_RULE_NONZERO, +MOVE_TO, 0, 8.33f, +CUBIC_TO, 4.5f, 8.33f, 8.33f, 4.5f, 8.33f, 0, +H_LINE_TO, 9.67f, +CUBIC_TO, 9.67f, 4.5f, 13.5f, 8.33f, 18, 8.33f, +V_LINE_TO, 9.67f, +CUBIC_TO, 13.5f, 9.67f, 9.67f, 13.5f, 9.67f, 18, +H_LINE_TO, 8.33f, +CUBIC_TO, 8.33f, 13.5f, 4.5f, 9.67f, 0, 9.67f, +V_LINE_TO, 8.33f, +CLOSE, +NEW_PATH \ No newline at end of file diff --git a/chrome/app/vector_icons/more_horiz.icon b/chrome/app/vector_icons/more_horiz.icon new file mode 100644 index 00000000000000..67504624591873 --- /dev/null +++ b/chrome/app/vector_icons/more_horiz.icon @@ -0,0 +1,32 @@ +CANVAS_DIMENSIONS, 20, +FILL_RULE_NONZERO, +MOVE_TO, 5.5f, 11.5f, +CUBIC_TO, 5.08f, 11.5f, 4.73f, 11.35f, 4.44f, 11.06f, +CUBIC_TO, 4.15f, 10.76f, 4, 10.41f, 4, 10, +CUBIC_TO, 4, 9.58f, 4.15f, 9.23f, 4.44f, 8.94f, +CUBIC_TO, 4.74f, 8.65f, 5.09f, 8.5f, 5.5f, 8.5f, +CUBIC_TO, 5.92f, 8.5f, 6.27f, 8.65f, 6.56f, 8.94f, +CUBIC_TO, 6.85f, 9.24f, 7, 9.59f, 7, 10, +CUBIC_TO, 7, 10.42f, 6.85f, 10.77f, 6.56f, 11.06f, +CUBIC_TO, 6.26f, 11.35f, 5.91f, 11.5f, 5.5f, 11.5f, +CLOSE, +MOVE_TO, 10, 11.5f, +CUBIC_TO, 9.58f, 11.5f, 9.23f, 11.35f, 8.94f, 11.06f, +CUBIC_TO, 8.65f, 10.76f, 8.5f, 10.41f, 8.5f, 10, +CUBIC_TO, 8.5f, 9.58f, 8.65f, 9.23f, 8.94f, 8.94f, +CUBIC_TO, 9.24f, 8.65f, 9.59f, 8.5f, 10, 8.5f, +CUBIC_TO, 10.42f, 8.5f, 10.77f, 8.65f, 11.06f, 8.94f, +CUBIC_TO, 11.35f, 9.24f, 11.5f, 9.59f, 11.5f, 10, +CUBIC_TO, 11.5f, 10.42f, 11.35f, 10.77f, 11.06f, 11.06f, +CUBIC_TO, 10.76f, 11.35f, 10.41f, 11.5f, 10, 11.5f, +CLOSE, +MOVE_TO, 14.5f, 11.5f, +CUBIC_TO, 14.08f, 11.5f, 13.73f, 11.35f, 13.44f, 11.06f, +CUBIC_TO, 13.15f, 10.76f, 13, 10.41f, 13, 10, +CUBIC_TO, 13, 9.58f, 13.15f, 9.23f, 13.44f, 8.94f, +CUBIC_TO, 13.74f, 8.65f, 14.09f, 8.5f, 14.5f, 8.5f, +CUBIC_TO, 14.92f, 8.5f, 15.27f, 8.65f, 15.56f, 8.94f, +CUBIC_TO, 15.85f, 9.24f, 16, 9.59f, 16, 10, +CUBIC_TO, 16, 10.42f, 15.85f, 10.77f, 15.56f, 11.06f, +CUBIC_TO, 15.26f, 11.35f, 14.91f, 11.5f, 14.5f, 11.5f, +CLOSE \ No newline at end of file diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn index d3564c952db123..1cadec04e9d3c9 100644 --- a/chrome/browser/BUILD.gn +++ b/chrome/browser/BUILD.gn @@ -1996,6 +1996,8 @@ static_library("browser") { "//chrome/common:supervised_user_commands_mojom", "//chrome/common:version_header", "//chrome/common/cart:mojo_bindings", + "//chrome/common/chat:mojo_bindings", + "//chrome/browser/ui/webui/side_panel/chat:mojo_bindings", "//chrome/common/net", "//chrome/common/notifications", "//chrome/installer/util:with_no_strings", diff --git a/chrome/browser/chrome_browser_interface_binders.cc b/chrome/browser/chrome_browser_interface_binders.cc index fefcf21bf3ffda..dfbf0a31736132 100644 --- a/chrome/browser/chrome_browser_interface_binders.cc +++ b/chrome/browser/chrome_browser_interface_binders.cc @@ -52,6 +52,8 @@ #include "chrome/browser/ui/webui/suggest_internals/suggest_internals_ui.h" #include "chrome/browser/ui/webui/usb_internals/usb_internals.mojom.h" #include "chrome/browser/ui/webui/usb_internals/usb_internals_ui.h" +#include "chrome/browser/ui/webui/side_panel/chat/chat.mojom.h" +#include "chrome/browser/ui/webui/side_panel/chat/chat_ui.h" #include "chrome/browser/web_applications/web_app_utils.h" #include "chrome/common/buildflags.h" #include "chrome/common/chrome_features.h" @@ -1242,7 +1244,7 @@ void PopulateChromeWebUIFrameBinders( policy::local_user_files::LocalFilesMigrationUI, #endif NewTabPageUI, OmniboxPopupUI, BookmarksSidePanelUI, CustomizeChromeUI, - InternalsUI, ReadingListUI, TabSearchUI, WebuiGalleryUI, + InternalsUI, ReadingListUI, ChatUI, TabSearchUI, WebuiGalleryUI, HistoryClustersSidePanelUI, ShoppingInsightsSidePanelUI, media_router::AccessCodeCastUI, commerce::ProductSpecificationsUI>(map); @@ -1374,6 +1376,9 @@ void PopulateChromeWebUIFrameBinders( side_panel::mojom::BookmarksPageHandlerFactory, BookmarksSidePanelUI>( map); + RegisterWebUIControllerInterfaceBinder< + chat::mojom::PageHandlerFactory, ChatUI>(map); + RegisterWebUIControllerInterfaceBinder< shopping_service::mojom::ShoppingServiceHandlerFactory, BookmarksSidePanelUI, commerce::ProductSpecificationsUI, diff --git a/chrome/browser/chrome_content_browser_client_receiver_bindings.cc b/chrome/browser/chrome_content_browser_client_receiver_bindings.cc index 0340dc97e87cdb..51a516ab04e4de 100644 --- a/chrome/browser/chrome_content_browser_client_receiver_bindings.cc +++ b/chrome/browser/chrome_content_browser_client_receiver_bindings.cc @@ -389,244 +389,244 @@ void ChromeContentBrowserClient:: RegisterAssociatedInterfaceBindersForRenderFrameHost( content::RenderFrameHost& render_frame_host, blink::AssociatedInterfaceRegistry& associated_registry) { - for (auto& ep : extra_parts_) { - ep->ExposeInterfacesToRendererForRenderFrameHost(render_frame_host, - associated_registry); - } + for (auto &ep: extra_parts_) { + ep->ExposeInterfacesToRendererForRenderFrameHost(render_frame_host, + associated_registry); + } - associated_registry.AddInterface( - base::BindRepeating( - &autofill::ContentAutofillDriverFactory::BindAutofillDriver, - &render_frame_host)); - associated_registry.AddInterface( - base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver< - autofill::mojom::PasswordGenerationDriver> receiver) { - ChromePasswordManagerClient::BindPasswordGenerationDriver( - std::move(receiver), render_frame_host); - }, - &render_frame_host)); - associated_registry.AddInterface< - autofill::mojom::PasswordManagerDriver>(base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver - receiver) { - password_manager::ContentPasswordManagerDriverFactory:: - BindPasswordManagerDriver(std::move(receiver), render_frame_host); - }, - &render_frame_host)); - associated_registry.AddInterface( - base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver - receiver) { - chrome_browser_net::NetErrorTabHelper::BindNetworkDiagnostics( - std::move(receiver), render_frame_host); - }, - &render_frame_host)); - associated_registry.AddInterface( - base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver - receiver) { - chrome_browser_net::NetErrorTabHelper::BindNetworkEasterEgg( - std::move(receiver), render_frame_host); - }, - &render_frame_host)); - associated_registry.AddInterface( - base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver - receiver) { - chrome_browser_net::NetErrorTabHelper::BindNetErrorPageSupport( - std::move(receiver), render_frame_host); - }, - &render_frame_host)); + associated_registry.AddInterface( + base::BindRepeating( + &autofill::ContentAutofillDriverFactory::BindAutofillDriver, + &render_frame_host)); + associated_registry.AddInterface( + base::BindRepeating( + [](content::RenderFrameHost *render_frame_host, + mojo::PendingAssociatedReceiver< + autofill::mojom::PasswordGenerationDriver> receiver) { + ChromePasswordManagerClient::BindPasswordGenerationDriver( + std::move(receiver), render_frame_host); + }, + &render_frame_host)); + associated_registry.AddInterface< + autofill::mojom::PasswordManagerDriver>(base::BindRepeating( + [](content::RenderFrameHost *render_frame_host, + mojo::PendingAssociatedReceiver + receiver) { + password_manager::ContentPasswordManagerDriverFactory:: + BindPasswordManagerDriver(std::move(receiver), render_frame_host); + }, + &render_frame_host)); + associated_registry.AddInterface( + base::BindRepeating( + [](content::RenderFrameHost *render_frame_host, + mojo::PendingAssociatedReceiver + receiver) { + chrome_browser_net::NetErrorTabHelper::BindNetworkDiagnostics( + std::move(receiver), render_frame_host); + }, + &render_frame_host)); + associated_registry.AddInterface( + base::BindRepeating( + [](content::RenderFrameHost *render_frame_host, + mojo::PendingAssociatedReceiver + receiver) { + chrome_browser_net::NetErrorTabHelper::BindNetworkEasterEgg( + std::move(receiver), render_frame_host); + }, + &render_frame_host)); + associated_registry.AddInterface( + base::BindRepeating( + [](content::RenderFrameHost *render_frame_host, + mojo::PendingAssociatedReceiver + receiver) { + chrome_browser_net::NetErrorTabHelper::BindNetErrorPageSupport( + std::move(receiver), render_frame_host); + }, + &render_frame_host)); #if BUILDFLAG(ENABLE_PLUGINS) - associated_registry.AddInterface< - chrome::mojom::PluginAuthHost>(base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver - receiver) { - extensions::ChromeWebViewPermissionHelperDelegate::BindPluginAuthHost( - std::move(receiver), render_frame_host); - }, - &render_frame_host)); + associated_registry.AddInterface< + chrome::mojom::PluginAuthHost>(base::BindRepeating( + [](content::RenderFrameHost* render_frame_host, + mojo::PendingAssociatedReceiver + receiver) { + extensions::ChromeWebViewPermissionHelperDelegate::BindPluginAuthHost( + std::move(receiver), render_frame_host); + }, + &render_frame_host)); #endif #if BUILDFLAG(ENABLE_PLUGINS) || BUILDFLAG(IS_ANDROID) #if BUILDFLAG(IS_ANDROID) - using PluginObserverImpl = PluginObserverAndroid; + using PluginObserverImpl = PluginObserverAndroid; #else - using PluginObserverImpl = PluginObserver; + using PluginObserverImpl = PluginObserver; #endif - associated_registry.AddInterface( - base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver - receiver) { - PluginObserverImpl::BindPluginHost(std::move(receiver), - render_frame_host); - }, - &render_frame_host)); + associated_registry.AddInterface( + base::BindRepeating( + [](content::RenderFrameHost* render_frame_host, + mojo::PendingAssociatedReceiver + receiver) { + PluginObserverImpl::BindPluginHost(std::move(receiver), + render_frame_host); + }, + &render_frame_host)); #endif // BUILDFLAG(ENABLE_PLUGINS) || BUILDFLAG(IS_ANDROID) - associated_registry.AddInterface< - chrome::mojom::TrustedVaultEncryptionKeysExtension>(base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver< - chrome::mojom::TrustedVaultEncryptionKeysExtension> receiver) { - TrustedVaultEncryptionKeysTabHelper:: - BindTrustedVaultEncryptionKeysExtension(std::move(receiver), - render_frame_host); - }, - &render_frame_host)); - associated_registry.AddInterface< - chrome::mojom::GoogleAccountsPrivateApiExtension>(base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver< - chrome::mojom::GoogleAccountsPrivateApiExtension> receiver) { - GoogleAccountsPrivateApiHost::BindHost(std::move(receiver), - render_frame_host); - }, - &render_frame_host)); - associated_registry.AddInterface< - content_capture::mojom::ContentCaptureReceiver>(base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver< - content_capture::mojom::ContentCaptureReceiver> receiver) { - content_capture::OnscreenContentProvider::BindContentCaptureReceiver( - std::move(receiver), render_frame_host); - }, - &render_frame_host)); + associated_registry.AddInterface< + chrome::mojom::TrustedVaultEncryptionKeysExtension>(base::BindRepeating( + [](content::RenderFrameHost *render_frame_host, + mojo::PendingAssociatedReceiver< + chrome::mojom::TrustedVaultEncryptionKeysExtension> receiver) { + TrustedVaultEncryptionKeysTabHelper:: + BindTrustedVaultEncryptionKeysExtension(std::move(receiver), + render_frame_host); + }, + &render_frame_host)); + associated_registry.AddInterface< + chrome::mojom::GoogleAccountsPrivateApiExtension>(base::BindRepeating( + [](content::RenderFrameHost *render_frame_host, + mojo::PendingAssociatedReceiver< + chrome::mojom::GoogleAccountsPrivateApiExtension> receiver) { + GoogleAccountsPrivateApiHost::BindHost(std::move(receiver), + render_frame_host); + }, + &render_frame_host)); + associated_registry.AddInterface< + content_capture::mojom::ContentCaptureReceiver>(base::BindRepeating( + [](content::RenderFrameHost *render_frame_host, + mojo::PendingAssociatedReceiver< + content_capture::mojom::ContentCaptureReceiver> receiver) { + content_capture::OnscreenContentProvider::BindContentCaptureReceiver( + std::move(receiver), render_frame_host); + }, + &render_frame_host)); #if BUILDFLAG(ENABLE_EXTENSIONS_CORE) - associated_registry.AddInterface( - base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver - receiver) { - extensions::ExtensionWebContentsObserver::BindLocalFrameHost( - std::move(receiver), render_frame_host); - }, - &render_frame_host)); + associated_registry.AddInterface( + base::BindRepeating( + [](content::RenderFrameHost* render_frame_host, + mojo::PendingAssociatedReceiver + receiver) { + extensions::ExtensionWebContentsObserver::BindLocalFrameHost( + std::move(receiver), render_frame_host); + }, + &render_frame_host)); #endif // BUILDFLAG(ENABLE_EXTENSIONS_CORE) #if BUILDFLAG(ENABLE_OFFLINE_PAGES) - associated_registry.AddInterface( - base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver< - offline_pages::mojom::MhtmlPageNotifier> receiver) { - offline_pages::OfflinePageTabHelper::BindHtmlPageNotifier( - std::move(receiver), render_frame_host); - }, - &render_frame_host)); + associated_registry.AddInterface( + base::BindRepeating( + [](content::RenderFrameHost* render_frame_host, + mojo::PendingAssociatedReceiver< + offline_pages::mojom::MhtmlPageNotifier> receiver) { + offline_pages::OfflinePageTabHelper::BindHtmlPageNotifier( + std::move(receiver), render_frame_host); + }, + &render_frame_host)); #endif // BUILDFLAG(ENABLE_OFFLINE_PAGES) - associated_registry.AddInterface( - base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver< - page_load_metrics::mojom::PageLoadMetrics> receiver) { - page_load_metrics::MetricsWebContentsObserver::BindPageLoadMetrics( - std::move(receiver), render_frame_host); - }, - &render_frame_host)); + associated_registry.AddInterface( + base::BindRepeating( + [](content::RenderFrameHost *render_frame_host, + mojo::PendingAssociatedReceiver< + page_load_metrics::mojom::PageLoadMetrics> receiver) { + page_load_metrics::MetricsWebContentsObserver::BindPageLoadMetrics( + std::move(receiver), render_frame_host); + }, + &render_frame_host)); #if BUILDFLAG(ENABLE_PDF) - associated_registry.AddInterface(base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver receiver) { - pdf::PDFDocumentHelper::BindPdfHost( - std::move(receiver), render_frame_host, - std::make_unique()); - }, - &render_frame_host)); + associated_registry.AddInterface(base::BindRepeating( + [](content::RenderFrameHost* render_frame_host, + mojo::PendingAssociatedReceiver receiver) { + pdf::PDFDocumentHelper::BindPdfHost( + std::move(receiver), render_frame_host, + std::make_unique()); + }, + &render_frame_host)); #endif // BUILDFLAG(ENABLE_PDF) #if !BUILDFLAG(IS_ANDROID) - associated_registry.AddInterface( - base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver< - search::mojom::EmbeddedSearchConnector> receiver) { - SearchTabHelper::BindEmbeddedSearchConnecter(std::move(receiver), - render_frame_host); - }, - &render_frame_host)); + associated_registry.AddInterface( + base::BindRepeating( + [](content::RenderFrameHost *render_frame_host, + mojo::PendingAssociatedReceiver< + search::mojom::EmbeddedSearchConnector> receiver) { + SearchTabHelper::BindEmbeddedSearchConnecter(std::move(receiver), + render_frame_host); + }, + &render_frame_host)); #endif // !BUILDFLAG(IS_ANDROID) #if BUILDFLAG(ENABLE_PRINTING) - associated_registry.AddInterface( - base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver - receiver) { - if (headless::IsHeadlessMode()) { - headless::HeadlessPrintManager::BindPrintManagerHost( - std::move(receiver), render_frame_host); - } else { + associated_registry.AddInterface( + base::BindRepeating( + [](content::RenderFrameHost* render_frame_host, + mojo::PendingAssociatedReceiver + receiver) { + if (headless::IsHeadlessMode()) { + headless::HeadlessPrintManager::BindPrintManagerHost( + std::move(receiver), render_frame_host); + } else { #if BUILDFLAG(IS_CHROMEOS) - if (base::FeatureList::IsEnabled( - ::features::kPrintPreviewCrosPrimary)) { + if (base::FeatureList::IsEnabled( + ::features::kPrintPreviewCrosPrimary)) { #if BUILDFLAG(ENABLE_PRINT_PREVIEW) - chromeos::PrintViewManagerCros::BindPrintManagerHost( - std::move(receiver), render_frame_host); + chromeos::PrintViewManagerCros::BindPrintManagerHost( + std::move(receiver), render_frame_host); #else - chromeos::PrintViewManagerCrosBasic::BindPrintManagerHost( - std::move(receiver), render_frame_host); + chromeos::PrintViewManagerCrosBasic::BindPrintManagerHost( + std::move(receiver), render_frame_host); #endif // BUILDFLAG(ENABLE_PRINT_PREVIEW) - return; - } + return; + } #endif // BUILDFLAG(CHROMEOS) #if BUILDFLAG(ENABLE_PRINT_PREVIEW) - printing::PrintViewManager::BindPrintManagerHost( - std::move(receiver), render_frame_host); + printing::PrintViewManager::BindPrintManagerHost( + std::move(receiver), render_frame_host); #else - printing::PrintViewManagerBasic::BindPrintManagerHost( - std::move(receiver), render_frame_host); + printing::PrintViewManagerBasic::BindPrintManagerHost( + std::move(receiver), render_frame_host); #endif // BUILDFLAG(ENABLE_PRINT_PREVIEW) - } - }, - &render_frame_host)); + } + }, + &render_frame_host)); #endif // BUILDFLAG(ENABLE_PRINTING) - associated_registry.AddInterface< - security_interstitials::mojom::InterstitialCommands>(base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver< - security_interstitials::mojom::InterstitialCommands> receiver) { - security_interstitials::SecurityInterstitialTabHelper:: - BindInterstitialCommands(std::move(receiver), render_frame_host); - }, - &render_frame_host)); - associated_registry.AddInterface< - subresource_filter::mojom::SubresourceFilterHost>(base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver< - subresource_filter::mojom::SubresourceFilterHost> receiver) { - subresource_filter::ContentSubresourceFilterThrottleManager:: - BindReceiver(std::move(receiver), render_frame_host); - }, - &render_frame_host)); - if (fingerprinting_protection_filter::features:: - IsFingerprintingProtectionFeatureEnabled()) { associated_registry.AddInterface< - fingerprinting_protection_filter::mojom::FingerprintingProtectionHost>( - base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, + security_interstitials::mojom::InterstitialCommands>(base::BindRepeating( + [](content::RenderFrameHost *render_frame_host, mojo::PendingAssociatedReceiver< - fingerprinting_protection_filter::mojom:: - FingerprintingProtectionHost> receiver) { - fingerprinting_protection_filter::ThrottleManager::BindReceiver( - std::move(receiver), render_frame_host); + security_interstitials::mojom::InterstitialCommands> receiver) { + security_interstitials::SecurityInterstitialTabHelper:: + BindInterstitialCommands(std::move(receiver), render_frame_host); }, &render_frame_host)); - } - associated_registry - .AddInterface( - base::BindRepeating( - [](content::RenderFrameHost* render_frame_host, - mojo::PendingAssociatedReceiver< - supervised_user::mojom::SupervisedUserCommands> receiver) { - SupervisedUserNavigationObserver::BindSupervisedUserCommands( - std::move(receiver), render_frame_host); - }, - &render_frame_host)); + associated_registry.AddInterface< + subresource_filter::mojom::SubresourceFilterHost>(base::BindRepeating( + [](content::RenderFrameHost *render_frame_host, + mojo::PendingAssociatedReceiver< + subresource_filter::mojom::SubresourceFilterHost> receiver) { + subresource_filter::ContentSubresourceFilterThrottleManager:: + BindReceiver(std::move(receiver), render_frame_host); + }, + &render_frame_host)); + if (fingerprinting_protection_filter::features:: + IsFingerprintingProtectionFeatureEnabled()) { + associated_registry.AddInterface< + fingerprinting_protection_filter::mojom::FingerprintingProtectionHost>( + base::BindRepeating( + [](content::RenderFrameHost *render_frame_host, + mojo::PendingAssociatedReceiver< + fingerprinting_protection_filter::mojom:: + FingerprintingProtectionHost> receiver) { + fingerprinting_protection_filter::ThrottleManager::BindReceiver( + std::move(receiver), render_frame_host); + }, + &render_frame_host)); + } + associated_registry + .AddInterface( + base::BindRepeating( + [](content::RenderFrameHost *render_frame_host, + mojo::PendingAssociatedReceiver< + supervised_user::mojom::SupervisedUserCommands> receiver) { + SupervisedUserNavigationObserver::BindSupervisedUserCommands( + std::move(receiver), render_frame_host); + }, + &render_frame_host)); } void ChromeContentBrowserClient::BindGpuHostReceiver( diff --git a/chrome/browser/resources/BUILD.gn b/chrome/browser/resources/BUILD.gn index 8f314e71ac57e5..be73d47afa1bfb 100644 --- a/chrome/browser/resources/BUILD.gn +++ b/chrome/browser/resources/BUILD.gn @@ -61,6 +61,7 @@ group("resources") { "side_panel/history_clusters:resources", "side_panel/read_anything:resources", "side_panel/reading_list:resources", + "side_panel/chat:resources", "side_panel/shared:resources", "signin:resources", "suggest_internals:resources", diff --git a/chrome/browser/resources/new_tab_page/new_tab_page.html b/chrome/browser/resources/new_tab_page/new_tab_page.html index 6e261416165ac0..072e28a5e6dca1 100644 --- a/chrome/browser/resources/new_tab_page/new_tab_page.html +++ b/chrome/browser/resources/new_tab_page/new_tab_page.html @@ -26,7 +26,6 @@ - diff --git a/chrome/browser/resources/side_panel/chat/BUILD.gn b/chrome/browser/resources/side_panel/chat/BUILD.gn new file mode 100644 index 00000000000000..a12444621b5a40 --- /dev/null +++ b/chrome/browser/resources/side_panel/chat/BUILD.gn @@ -0,0 +1,46 @@ +import("//ui/webui/resources/tools/build_webui.gni") + +assert(!is_android) + +build_webui("build") { + grd_prefix = "side_panel_chat" + + static_files = [ "chat.html" ] + + non_web_component_files = [ + "chat_api_proxy.ts", + "chat_app.ts", + "chat_app.html.ts", + "marked.ts", + "chat_prompt_input.html.ts", + "chat_prompt_input.ts", + "action_menu.ts", + "action_menu.html.ts", + ] + + css_files = [ + "chat_app.css", + "chat_prompt_input.css", + "action_menu.css", + ] + + mojo_files_deps = [ + "//chrome/browser/ui/webui/side_panel/chat:mojo_bindings_ts__generator", + ] + + mojo_files = [ + "$root_gen_dir/chrome/browser/ui/webui/side_panel/chat/chat.mojom-webui.ts", + ] + + ts_composite = true + + ts_deps = [ + "//third_party/lit/v3_0:build_ts", + "//ui/webui/resources/cr_components/color_change_listener:build_ts", + "//ui/webui/resources/cr_elements:build_ts", + "//ui/webui/resources/js:build_ts", + "//ui/webui/resources/mojo:build_ts", + ] + + webui_context_type = "trusted" +} diff --git a/chrome/browser/resources/side_panel/chat/action_menu.css b/chrome/browser/resources/side_panel/chat/action_menu.css new file mode 100644 index 00000000000000..65e508bc4a1442 --- /dev/null +++ b/chrome/browser/resources/side_panel/chat/action_menu.css @@ -0,0 +1,17 @@ +/* #css_wrapper_metadata_start + * #type=style-lit + * #import=../action_menu.css.js + * #scheme=relative + * #css_wrapper_metadata_end */ + +.action-button { + display: block; + padding: 10px 12px; + border: 1px solid var(--color-yep-chat-panel-border); + border-radius: 12px; + font-weight: 500; + cursor: pointer; + text-decoration: none; + background: var(--color-yep-chat-panel-background); + color: var(--color-yep-chat-primary-text); +} diff --git a/chrome/browser/resources/side_panel/chat/action_menu.html.ts b/chrome/browser/resources/side_panel/chat/action_menu.html.ts new file mode 100644 index 00000000000000..5a56df9ca2ed16 --- /dev/null +++ b/chrome/browser/resources/side_panel/chat/action_menu.html.ts @@ -0,0 +1,24 @@ +import {html} from '//resources/lit/v3_0/lit.rollup.js'; + +import type {ActionMenuElement} from './action_menu.js'; + +export function getHtml(this: ActionMenuElement) { + return html` + + + ${this.renderActionMenu_ ? html` + + ${this.actionItems_.map((item, _) => { + return html` + `; + })} + ` : html``}`; +} diff --git a/chrome/browser/resources/side_panel/chat/action_menu.ts b/chrome/browser/resources/side_panel/chat/action_menu.ts new file mode 100644 index 00000000000000..6189ccd24e16f6 --- /dev/null +++ b/chrome/browser/resources/side_panel/chat/action_menu.ts @@ -0,0 +1,76 @@ +import '//resources/cr_elements/cr_action_menu/cr_action_menu.js'; +import '//resources/cr_elements/cr_dialog/cr_dialog.js'; +import {CrLitElement} from '//resources/lit/v3_0/lit.rollup.js'; +import {assert} from '//resources/js/assert.js'; +import {getCss} from './action_menu.css.js'; +import {getHtml} from './action_menu.html.js'; +import {ActionType} from "./chat.mojom-webui.js"; +import {AnchorAlignment} from '//resources/cr_elements/cr_action_menu/cr_action_menu.js'; + +export interface ActionMenuElement { + $: { + actionMenuButton: HTMLElement, + }; +} + +export class ActionMenuElement extends CrLitElement { + protected renderActionMenu_: boolean = false; + protected actionType_: ActionType = ActionType.NONE; + protected actionLabel_: string = ''; + protected actionItems_: string[] = []; + protected disabled_: boolean = false; + + constructor() { + super(); + } + + static get is() { + return 'action-menu'; + } + + static override get styles() { + return getCss(); + } + + static override get properties() { + return { + actionType_: {type: ActionType}, + actionLabel_: {type: String}, + actionItems_: {type: Array}, + renderActionMenu_: {type: Boolean}, + disabled_: {type: Boolean}, + }; + } + + override render() { + return getHtml.bind(this)(); + } + + protected async onActionMenuButtonClick_(event: Event) { + event.preventDefault(); // Prevent default browser action (navigation). + if (!this.renderActionMenu_) { + this.renderActionMenu_ = true; + await this.updateComplete; + } + const menu = this.shadowRoot!.querySelector('cr-action-menu'); + assert(menu); + menu.showAt(this.$.actionMenuButton, { + anchorAlignmentX: AnchorAlignment.AFTER_END, + anchorAlignmentY: AnchorAlignment.CENTER, + } + ); + } + + protected onActionMenuItemClick_(action_param: string) { + this.fire("item-click", {actionType: this.actionType_, actionParam: action_param}) + } +} + + +declare global { + interface HTMLElementTagNameMap { + 'action-menu': ActionMenuElement; + } +} + +customElements.define(ActionMenuElement.is, ActionMenuElement); diff --git a/chrome/browser/resources/side_panel/chat/chat.html b/chrome/browser/resources/side_panel/chat/chat.html new file mode 100644 index 00000000000000..4a7e489aaefc6b --- /dev/null +++ b/chrome/browser/resources/side_panel/chat/chat.html @@ -0,0 +1,23 @@ + + + + + $i18n{title} + + + + + + + + + \ No newline at end of file diff --git a/chrome/browser/resources/side_panel/chat/chat_api_proxy.ts b/chrome/browser/resources/side_panel/chat/chat_api_proxy.ts new file mode 100644 index 00000000000000..658428bf0015ec --- /dev/null +++ b/chrome/browser/resources/side_panel/chat/chat_api_proxy.ts @@ -0,0 +1,91 @@ +import { + ActionItem, + ActionType, + PageCallbackRouter, + PageHandlerFactory, + PageHandlerRemote, + SiteInfo, + ConversationItem, +} from "./chat.mojom-webui.js"; + +import type {ClickModifiers} from 'chrome://resources/mojo/ui/base/mojom/window_open_disposition.mojom-webui.js'; + +export interface ChatApiProxy { + getActionList(): Promise<{ actionList: ActionItem[] }>; + + submitAction(actionType: ActionType, actionParam: string): void; + + submitQuery(actionType: ActionType, query: string, url: string, conversation_history : ConversationItem[]): void; + + getSiteInfo(): Promise<{ siteInfo: SiteInfo }> + + showUI(): void; + + closeUI(): void; + + cancelQuery(): void; + + openUrl(url: string, clickModifiers: ClickModifiers): void; + + getCallbackRouter(): PageCallbackRouter; +} + +let instance: ChatApiProxy | null = null; + +export class ChatApiProxyImpl implements ChatApiProxy { + private readonly callbackRouter: PageCallbackRouter = new PageCallbackRouter(); + private handler: PageHandlerRemote = new PageHandlerRemote(); + + constructor() { + this.callbackRouter = new PageCallbackRouter(); + this.handler = new PageHandlerRemote(); + const factory = PageHandlerFactory.getRemote(); + factory.createPageHandler( + this.callbackRouter.$.bindNewPipeAndPassRemote(), + this.handler.$.bindNewPipeAndPassReceiver()); + } + + static getInstance(): ChatApiProxy { + return instance || (instance = new ChatApiProxyImpl()); + } + + static setInstance(proxy: ChatApiProxy) { + instance = proxy; + } + + getActionList(): Promise<{ actionList: ActionItem[] }> { + return this.handler.getActionList(); + } + + submitAction(actionType: ActionType, actionParam: string) { + this.handler.submitAction(actionType, actionParam); + } + + submitQuery(actionType: ActionType, query: string, url: string, conversation_history: ConversationItem[] ) { + this.handler.submitQuery(actionType, query, url, conversation_history); + } + + getSiteInfo() { + return this.handler.getSiteInfo(); + } + + showUI() { + this.handler.showUI(); + } + + closeUI() { + this.handler.closeUI(); + } + + cancelQuery() { + this.handler.cancelQuery(); + } + + openUrl(url: string, clickModifiers: ClickModifiers) { + this.handler.openURL(url, clickModifiers); + } + + getCallbackRouter() { + return this.callbackRouter; + } +} \ No newline at end of file diff --git a/chrome/browser/resources/side_panel/chat/chat_app.css b/chrome/browser/resources/side_panel/chat/chat_app.css new file mode 100644 index 00000000000000..de09ed278b6fd0 --- /dev/null +++ b/chrome/browser/resources/side_panel/chat/chat_app.css @@ -0,0 +1,478 @@ +/* #css_wrapper_metadata_start + * #type=style-lit + * #import=../chat_app.css.js + * #scheme=relative + * #css_wrapper_metadata_end */ + +#container { + display: flex; + flex-direction: column; + height: 99vh; + padding: 0 0 16px 0; + color: var(--color-yep-chat-primary-text); + box-sizing: border-box; + font-size: 13px; + line-height: 20px; + box-sizing: border-box; +} + +#header-container { + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + min-height: 48px; + padding: 0 16px 0 16px; + border-bottom: 1px solid var(--color-yep-chat-panel-border); + z-index: 10; +} + +#header-container .header-title-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + width: fit-content; + gap: 12px; +} + +#header-container .header-btn { + display: block; + border: none; + padding: 0; + margin: 0; + cursor: pointer; + background-color: transparent; +} + +#target-restart-btn:disabled { + cursor: not-allowed; +} + +.header-btn .header-icon { + display: block; + width: 20px; + height: 20px; + color: var(--color-yep-chat-secondary-text); +} + +.header-title-container .chat-title { + display: block; + font-weight: 500; + font-size: 16px; +} + +.chat-scroller { + display: block; + overflow-x: hidden; + overflow-y: auto; + scrollbar-gutter: stable; + scroll-behavior: smooth; + margin-block-start: 10px; +} + +.chat-scroller::-webkit-scrollbar { + background: transparent; + width: 8px; +} + +.chat-scroller::-webkit-scrollbar-thumb { + background: var(--color-yep-chat-site-info-vertical-bar-background); + background-clip: content-box; + border: solid 1.5px transparent; + border-radius: 100px; +} + +.chat-scroller-top-of-page::-webkit-scrollbar-track { + margin-block-start: 8px; +} + +#conversation-container { + flex-grow: 1; + display: flex; + flex-direction: column; + gap: 8px; + margin: 0 0 0 16px; + z-index: 0; + scroll-behavior: smooth; + will-change: scroll-position; + scrollbar-gutter: stable; + scroll-snap-stop: always; +} + +.conversation-content { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: 1px; + padding: 0 16px 0 0; +} + +.conversation-content .message-markdown-container { + display: block; + padding: 0 12px 0 12px; + background: var(--color-yep-chat-panel-background); + color: var(--color-yep-chat-primary-text); +} + +.vertical-space { + padding-top: 2px; +} + +.chat-about-this-page-btn { + display: flex; + justify-content: flex-start; + gap: 6px; + background-color: transparent; + cursor: pointer; + border: none; + padding: 0; + margin: 0; + width: fit-content; +} + +.chat-about-this-page-btn .add-icon-wrapper { + flex: 0 0 auto; + height: 14px; + border: none; + cursor: pointer; + background-color: transparent; + padding: 0; + margin: 0; +} + +.chat-about-this-page-btn .add-icon-wrapper .add-icon { + display: block; + width: 14px; + height: 14px; + color: var(--color-yep-chat-tertiary-text); +} + +.chat-about-this-page-btn .label { + flex: 0 0 auto; + font-weight: 510; + font-size: 12px; + color: var(--color-yep-chat-tertiary-text); +} + +.siteinfo-container { + display: flex; + flex-direction: row; + justify-content: flex-start; + width: auto; + max-width: 100%; + gap: 6px; + color: var(--color-yep-chat-primary-text); +} + +.siteinfo-button { + cursor: pointer; +} + +.vertical-bar { + display: block; + flex: 0 0 auto; + width: 4px; + border-radius: 3px; + background: var(--color-yep-chat-site-info-vertical-bar-background); +} + +.remove-siteinfo-button { + display: block; + flex: 0 0 auto; + width: 14px; + height: 14px; + background-color: transparent; + border: none; + cursor: pointer; + padding: 0; + margin: 0; +} + +.remove-siteinfo-button .remove-icon { + display: block; + width: 14px; + height: 14px; + color: var(--color-yep-chat-secondary-text); +} + +.siteinfo-container .siteinfo-content { + display: flex; + flex-direction: column; + flex-grow: 1; + font-weight: 400; + font-size: 12px; + line-height: 16px; + width: auto; + max-width: 97%; +} + +.siteinfo-container .siteinfo-content .siteinfo-title { + width: 95%; + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: top; + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.siteinfo-container .siteinfo-content .siteinfo-url { + width: 95%; + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: top; + margin: 0; + padding: 0; + box-sizing: border-box; + color: var(--color-yep-chat-secondary-text); +} + +.query-prompt-container { + display: flex; + flex-direction: column; + gap: 10px; + padding: 0 10px 8px 10px; + border-radius: 8px; + background: var(--color-yep-chat-prompt-background); +} + +.auto-width-with-padding { + width: auto; + padding-top: 10px; + margin-right: 4px; +} + +.content-fit-width { + width: fit-content; +} + +.top-padding { + padding-top: 8px; +} + +.query-prompt-container .prompt-section { + font-weight: 400; + width: fit-content; + word-wrap: break-word; + box-sizing: border-box; + display: block; + overflow-wrap: anywhere; + text-size-adjust: 100%; + text-wrap: wrap; + unicode-bidi: isolate; + white-space: pre-wrap; + white-space-collapse: preserve; +} + +.action-buttons-container { + display: flex; + gap: 6px; + flex-direction: column; + align-items: flex-start; + padding-bottom: 6px; +} + +.action-button { + display: block; + padding: 10px 12px; + border: 1px solid var(--color-yep-chat-panel-border); + border-radius: 12px; + font-weight: 500; + cursor: pointer; + text-decoration: none; + background: var(--color-yep-chat-panel-background); + color: var(--color-yep-chat-primary-text); +} + +#prompt-container { + display: flex; + flex-direction: column; + border-radius: 12px; + padding: 12px 12px 12px 12px; + margin: 0 18px 0 16px; + gap: 8px; + background: var(--color-yep-chat-prompt-background); + border: 1px solid var(--color-yep-chat-panel-background); +} + +.typing-content { + display: flex; + flex-direction: row; + align-items: end; + justify-content: flex-start; + gap: 4px; + width: 100vw; + max-width: 100%; + background: transparent; +} + +.typing-content .prompt-input { + display: block; + flex-grow: 1; +} + +#error { + display: block; + font-size: 11px; + padding: 6px 12px 12px 12px; + margin: 0 16px 0 16px; + color: var(--color-yep-chat-error); +} + +.send-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border: none; + cursor: pointer; + padding: 0; + margin: 0; + border-radius: 50%; + color: var(--color-yep-chat-send-button-background); + background-color: var(--color-yep-chat-send-button-foreground); +} + +.send-icon-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.send-icon { + top: 2px; + left: 2px; + width: 22px; + height: 22px; +} + +.send-btn:disabled { + cursor: not-allowed; + color: var(--color-yep-chat-tertiary-text); +} + +.message-markdown-container p { + font-weight: 400; + line-height: 18px; +} + +.message-markdown-container a { + text-decoration: none; + color: inherit; + cursor: default; +} + +.message-markdown-container a:hover { + text-decoration: none; + color: inherit; +} + +.message-markdown-container h1 { + font-weight: 700; + font-size: 22px; + line-height: 26px; +} + +.message-markdown-container h2 { + font-weight: 700; + font-size: 17px; + line-height: 22px; +} + +.message-markdown-container blockquote { + border-left: 5px solid var(--color-yep-chat-site-info-vertical-bar-background); + margin: 0; + padding: 0 0 0 6px; +} + +.message-markdown-container code { + font-size: 12px; + font-weight: 400; + line-height: 16px; + border-radius: 2px; + padding: 4px 7px; + background: var(--color-yep-chat-prompt-background); + color: var(--color-yep-chat-primary-text); +} + +.message-markdown-container pre { + border-radius: 6px; + max-width: 90vw; + padding: 14px; + background: var(--color-yep-chat-prompt-background); + color: var(--color-yep-chat-primary-text); + overflow-x: auto; + overflow-y: auto; + scrollbar-gutter: stable; + scroll-behavior: smooth; +} + +.message-markdown-container pre::-webkit-scrollbar { + height: 8px; + background-color: transparent; +} + +.message-markdown-container pre::-webkit-scrollbar-thumb { + background-color: var(--color-yep-chat-site-info-vertical-bar-background); + background-clip: content-box; + border: solid 1.5px transparent; + border-radius: 100px; +} + +.message-markdown-container pre code { + line-height: 22px; + padding: 0; +} + +.message-markdown-container table { + border-collapse: collapse; +} + +.message-markdown-container table thead th { + text-align: left; +} + +.message-markdown-container table tr { + border: 1px solid var(--color-yep-chat-panel-border); + border-left: none; + border-right: none; +} + +.message-markdown-container table th, td { + padding: 10px; +} + +.message-markdown-container ul { + line-height: 18px; + list-style-type: disc; + padding-left: 14px; +} + +.message-markdown-container ol { + line-height: 18px; + list-style-type: decimal; + padding-left: 14px; +} + +.message-markdown-container ol ul, ul ol { + padding-left: 14px; +} + +.message-markdown-container ol ol { + list-style-type: lower-alpha; +} + +.message-markdown-container li { + margin-top: 10px; + margin-bottom: 10px; +} \ No newline at end of file diff --git a/chrome/browser/resources/side_panel/chat/chat_app.html.ts b/chrome/browser/resources/side_panel/chat/chat_app.html.ts new file mode 100644 index 00000000000000..4aab265a69c936 --- /dev/null +++ b/chrome/browser/resources/side_panel/chat/chat_app.html.ts @@ -0,0 +1,229 @@ +import 'chrome://resources/cr_elements/cr_icon/cr_icon.js'; +import 'chrome://resources/cr_elements/icons_lit.html.js'; +import {getTrustedHTML} from 'chrome://resources/js/parse_html_subset.js'; +import {ChatAppElement, conversationRecord, ActionOnExtractedContent } from "./chat_app.js"; +import {html} from '//resources/lit/v3_0/lit.rollup.js'; +import {marked} from "./marked.js"; +import {ActionType} from "./chat.mojom-webui.js"; +import type {ClickModifiers} from 'chrome://resources/mojo/ui/base/mojom/window_open_disposition.mojom-webui.js'; +import '//resources/cr_elements/cr_tooltip/cr_tooltip.js'; +import {TooltipPosition} from '//resources/cr_elements/cr_tooltip/cr_tooltip.js'; +import './chat_prompt_input.js'; +import './action_menu.js'; + +function getSiteInfoOrAddChatAboutThisPage(this: ChatAppElement) { + if (this.shouldDisplayChatAboutThisPageButton_ && this.siteInfo_.isContentUsableInConversations) { + return html` + `; + } else if (this.shouldHideSiteInfoContainerDueToKnownContext_) { + return html``; + } else if (!this.shouldDisplayChatAboutThisPageButton_ && this.siteInfo_.isContentUsableInConversations) { + return html` +
+ +
+
+
${this.siteInfo_.title}
+
+ ${this.stripUrlProtocol_(this.siteInfo_.url ?? "")} +
+
+
` + } else { + return html``; + } +} + +function getConversationResponseElement(conversation: conversationRecord) { + return conversation.response.length > 0 ? html` +
+
` : html``; +} + +function getQueryPromptSection(query: string) { + return html` +
${query}
`; +} + +export function getHtml(this: ChatAppElement) { + return html` +
+
+
+ + +
${this.title_}
+
+ + +
+
+
+
+ ${ + this.conversations_.map((conversation, _) => { + return conversation.query.length > 0 + ? html` + ${conversation.shouldDisplaySiteInfo ? html` +
+
+
+
+
${conversation.title}
+
${conversation.url}
+
+
+ ${getQueryPromptSection(conversation.query)} +
` : html` +
+ ${getQueryPromptSection(conversation.query)} +
` + } + ${getConversationResponseElement.bind(this)(conversation)}` + : + html`${getConversationResponseElement.bind(this)(conversation)}` + }) + } +
+
+
+
+ ${this.shouldUseCurrentPageContentAsChatContext_ && this.siteInfo_.isContentUsableInConversations && this.conversations_ && this.conversations_.length == 0 ? + this.actionList_.map((item, _) => { + if (item.actionType == ActionType.TRANSLATE) { + return html` + + `; + } else if (item.actionType == ActionType.DRAFT_SOCIAL_MEDIA_POST) { + return html` + + `; + } else { + return html` + `; + } + }) : html``} +
+
+
+ ${getSiteInfoOrAddChatAboutThisPage.bind(this)()} +
+
+ + +
+ +
+
+ ${this.hasExceededMaxTokenCount_ + ? html` +
${this.exceedMaxTokenCountErrorMessages_}
` + : html``} + ${this.hasErrorOccurred_ + ? html` +
${this.errorMessage_}
` + : html`` + } +
`; +} \ No newline at end of file diff --git a/chrome/browser/resources/side_panel/chat/chat_app.ts b/chrome/browser/resources/side_panel/chat/chat_app.ts new file mode 100644 index 00000000000000..2effe22e788cd8 --- /dev/null +++ b/chrome/browser/resources/side_panel/chat/chat_app.ts @@ -0,0 +1,401 @@ +import './strings.m.js'; +import '//resources/cr_elements/cr_button/cr_button.js'; +import '//resources/cr_elements/cr_icon_button/cr_icon_button.js'; +import '//resources/cr_elements/cr_input/cr_input.js'; +import '//resources/cr_elements/cr_textarea/cr_textarea.js'; +import '//resources/cr_elements/cr_action_menu/cr_action_menu.js'; +import '//resources/cr_elements/cr_dialog/cr_dialog.js'; +import {ColorChangeUpdater} from 'chrome://resources/cr_components/color_change_listener/colors_css_updater.js'; +import {loadTimeData} from 'chrome://resources/js/load_time_data.js'; +import {CrLitElement} from '//resources/lit/v3_0/lit.rollup.js'; +import {getCss} from './chat_app.css.js'; +import {getHtml} from './chat_app.html.js'; +import type {ChatApiProxy} from "./chat_api_proxy.js"; +import {ChatApiProxyImpl} from "./chat_api_proxy.js"; +import {ActionItem, ActionResponse, ActionType, ResponseType, SiteInfo, ConversationItem} from "./chat.mojom-webui.js"; +import {marked} from "./marked.js"; +import type {ChatPromptInputElement} from "./chat_prompt_input"; +import type {ClickModifiers} from 'chrome://resources/mojo/ui/base/mojom/window_open_disposition.mojom-webui.js'; +import './chat_prompt_input.js'; +import './action_menu.js'; + +function transformToArray(input: string): string[] { + return input.split(",").map(item => item.trim()); +} + +export enum ActionOnExtractedContent { + AddAsContext = 0, + RemoveFromUsingAsContext = 1, +} + +export type conversationRecord = { + query: string, + shouldDisplaySiteInfo: boolean, + title: string, + url: string, + response: string, +} + +export interface ChatAppElement { + $: { + promptInput: ChatPromptInputElement, + }; +} + +export class ChatAppElement extends CrLitElement { + private chatApiProxy_: ChatApiProxy = ChatApiProxyImpl.getInstance(); + private listenerIds_: number[] = []; + protected actionList_: ActionItem[] = []; + protected translateToLanguages_: string[] = []; + protected socialMediaPlatforms_: string[] = []; + protected conversations_: conversationRecord[] = []; + protected title_: string = loadTimeData.getString('title'); + protected askAnythingLabel_ = loadTimeData.getString('askAnything'); + protected chatAboutThisPageLabel_ = loadTimeData.getString('chatAboutThisPage'); + protected tooltipCloseSidePanel_ = loadTimeData.getString('closeSidePanel'); + protected tooltipNewChat_= loadTimeData.getString('newChat'); + protected siteInfo_: SiteInfo = { + url: "", + title: "", + isContentUsableInConversations: false, + }; + protected completionResult_: string = ""; + protected query_?: string; + protected submittedQuery_?: string; + protected isSubmittingQuery_: boolean = false; + protected shouldDisplayChatAboutThisPageButton_: boolean = false; + protected exceedMaxTokenCountErrorMessages_: string = ""; + protected hasExceededMaxTokenCount_: boolean = false; + protected errorMessage_: string = ""; + protected hasErrorOccurred_: boolean = false; + protected maxPromptInputLength_: number = 90_000; + protected shouldHideSiteInfoContainerDueToKnownContext_: boolean = false; + protected shouldUseCurrentPageContentAsChatContext_: boolean = false; + + constructor() { + super(); + this.translateToLanguages_ = transformToArray(loadTimeData.getString('translateLanguages')); + this.socialMediaPlatforms_ = transformToArray(loadTimeData.getString('socialMedias')); + } + + static get is() { + return 'chat-app'; + } + + static override get styles() { + return getCss(); + } + + override render() { + return getHtml.bind(this)(); + } + + static override get properties() { + return { + siteInfo_: {type: Object}, + askAnythingLabel_: {type: String}, + actionList_: {type: Array}, + translateToSubItems_: {type: String}, + socialMediaPostSubItems_: {type: String}, + query_: {type: String}, + submittedQuery_: {type: String}, + completionResult_: {type: String}, + isSubmittingQuery_: {type: Boolean}, + shouldDisplayChatAboutThisPageButton_: {type: Boolean}, + exceedMaxLengthErrorMessages_: {type: String}, + hasExceededMaxTokenCount: {type: Boolean}, + errorMessage_: {type: String}, + hasErrorOccurred_: {type: Boolean}, + maxPromptInputLength_: {type: Number}, + shouldHideSiteInfoContainerDueToKnownContext_: {type: Boolean}, + shouldUseCurrentPageContentAsChatContext_: {type: Boolean}, + title_: {type: String}, + tooltipCloseSidePanel_: {type: String}, + tooltipNewChat_: {type: String}, + }; + } + + private async updateSiteInfo(siteInfo: SiteInfo) { + if (this.siteInfo_.url === siteInfo.url) return; + + this.siteInfo_ = siteInfo; + if (this.siteInfo_.isContentUsableInConversations) { + const {actionList} = await this.chatApiProxy_.getActionList(); + this.actionList_ = actionList; + this.shouldUseCurrentPageContentAsChatContext_ = true; + this.shouldDisplayChatAboutThisPageButton_ = false; + this.hasErrorOccurred_ = false; + this.errorMessage_ = "" + } else { + this.shouldUseCurrentPageContentAsChatContext_ = false; + } + this.shouldHideSiteInfoContainerDueToKnownContext_ = false; + this.updateComplete; + } + + private updateSubmitResponse(response: ActionResponse) { + if (response.responseType == ResponseType.DELTA) { + this.completionResult_ += response.result; + this.hasErrorOccurred_ = false; + this.errorMessage_ = ""; + } else if (response.responseType == ResponseType.COMPLETED) { + this.completionResult_ += "\n"; + this.isSubmittingQuery_ = false; + this.hasErrorOccurred_ = false; + this.errorMessage_ = ""; + setTimeout(() => this.$.promptInput.focusInput(), 0); + } else if (response.responseType == ResponseType.ERROR) { + this.completionResult_ += "\n"; + this.isSubmittingQuery_ = false; + this.hasErrorOccurred_ = true; + this.errorMessage_ = loadTimeData.getString('genericError'); + setTimeout(() => this.$.promptInput.focusInput(), 0); + } + } + + protected onCloseSidePanel_(e: Event){ + e.preventDefault(); + this.chatApiProxy_.closeUI(); + } + + protected onRestartChat_(e: Event) { + e.preventDefault(); + + this.query_ = ""; + this.conversations_.length = 0; + this.completionResult_ = ""; + this.isSubmittingQuery_ = false; + this.submittedQuery_ = ""; + this.shouldDisplayChatAboutThisPageButton_ = false; + this.hasErrorOccurred_ = false; + this.hasExceededMaxTokenCount_ = false; + this.exceedMaxTokenCountErrorMessages_ = ""; + this.errorMessage_ = "" + this.shouldHideSiteInfoContainerDueToKnownContext_ = false; + if (this.siteInfo_.isContentUsableInConversations) { + this.shouldUseCurrentPageContentAsChatContext_ = true; + } else { + this.shouldUseCurrentPageContentAsChatContext_ = false; + } + this.$.promptInput.resetToAutoHeight(); + this.$.promptInput.focusInput(); + } + + protected onPerformActionOnExtractedContent_(action: ActionOnExtractedContent) { + if (action === ActionOnExtractedContent.AddAsContext) { + this.shouldDisplayChatAboutThisPageButton_ = false; + this.shouldUseCurrentPageContentAsChatContext_ = true; + } else if (action === ActionOnExtractedContent.RemoveFromUsingAsContext) { + this.shouldDisplayChatAboutThisPageButton_ = true; + this.shouldUseCurrentPageContentAsChatContext_ = false; + } + this.$.promptInput.focusInput(); + this.shouldHideSiteInfoContainerDueToKnownContext_ = false; + } + + protected stripUrlProtocol_(url: string = ''): string { + const PROTOCOL_REGEX = /^https?:\/\//; + return url ? url.replace(PROTOCOL_REGEX, '') : ''; + } + + protected onActionMenuItemClick_(e: CustomEvent<{ actionType: ActionType, actionParam: string }>) { + this.onSubmitAction_(e.detail.actionType, e.detail.actionParam); + } + + protected onSubmitAction_(actionType: ActionType, actionParam: string = '') { + const title = this.siteInfo_.title ?? ""; + const url = this.stripUrlProtocol_(this.siteInfo_.url ?? ""); + const response = ""; + if (this.conversations_ != null) { + if (actionType == ActionType.SUMMARIZE_PAGE) { + this.shouldHideSiteInfoContainerDueToKnownContext_ = true; + this.conversations_.push({ + query: loadTimeData.getString('promptSummarizeThisPage'), + shouldDisplaySiteInfo: true, + title, + url, + response, + }); + } else if (actionType == ActionType.EXPLAIN) { + this.shouldHideSiteInfoContainerDueToKnownContext_ = true; + this.conversations_.push({ + query: loadTimeData.getString('promptExplainInSimpleLanguage'), + shouldDisplaySiteInfo: true, + title, + url, + response, + }); + } else if (actionType == ActionType.FACT_CHECK) { + this.shouldHideSiteInfoContainerDueToKnownContext_ = true; + this.conversations_.push({ + query: loadTimeData.getString('promptFactCheck'), + shouldDisplaySiteInfo: true, + title, + url, + response, + }); + } else if (actionType == ActionType.TRANSLATE) { + this.shouldHideSiteInfoContainerDueToKnownContext_ = true; + this.conversations_.push({ + query: loadTimeData.getString('promptTranslate') + ' ' + actionParam, + shouldDisplaySiteInfo: true, + title, + url, + response, + }); + } else if (actionType == ActionType.DRAFT_SOCIAL_MEDIA_POST) { + this.shouldHideSiteInfoContainerDueToKnownContext_ = true; + this.conversations_.push({ + query: loadTimeData.getString('promptSocialMediaPost') + ' ' + actionParam, + shouldDisplaySiteInfo: true, + title, + url, + response, + }); + } else { + // this branch should not be reached + this.conversations_.push({ + query: "", + shouldDisplaySiteInfo: false, + title, + url, + response, + }); + } + } + this.isSubmittingQuery_ = true; + setTimeout(() => this.chatApiProxy_.submitAction(actionType, actionParam), 0); + } + + protected onPromptInputChange_(e: CustomEvent<{ value: string }>) { + this.query_ = e.detail.value; + + if (this.query_.length > this.maxPromptInputLength_) { + this.exceedMaxTokenCountErrorMessages_ = loadTimeData.getString("promptExceedMaxTokenCount") + " - " + this.query_.length + "/" + this.maxPromptInputLength_ + "."; + this.hasExceededMaxTokenCount_ = true; + } else { + this.exceedMaxTokenCountErrorMessages_ = ""; + this.hasExceededMaxTokenCount_ = false; + } + } + + protected onSetAndSubmitQuery_(e: CustomEvent<{ value: string }>) { + this.query_ = e.detail.value; + if (this.query_ !== "" && !this.hasExceededMaxTokenCount_) { + this.onSubmitQuery_(); + } + + // hide site info from prompt input because it's obvious that the content of currently opening site will be used as context + if ((this.siteInfo_.url != undefined && this.siteInfo_.url.length > 0) && this.siteInfo_.isContentUsableInConversations) { + this.shouldHideSiteInfoContainerDueToKnownContext_ = true; + } else { + this.shouldHideSiteInfoContainerDueToKnownContext_ = false; + } + } + + protected onSubmitQuery_() { + if (this.completionResult_ && this.completionResult_.length > 0) { + if (this.conversations_ != null) { + if (this.conversations_.length == 0) { + this.conversations_.push({ + query: this.query_ ?? "", + shouldDisplaySiteInfo: false, + title: this.siteInfo_.title ?? "", + url: this.siteInfo_.url ?? "", + response: marked.parse(this.completionResult_, {async: false}), + }); + } else { + const lastIndex = this.conversations_.length - 1; + const lastConversation = this.conversations_[lastIndex]; + if (lastConversation) { + lastConversation.response = marked.parse(this.completionResult_, {async: false}); + } + } + } + } + this.completionResult_ = ""; + this.submittedQuery_ = this.query_; + this.conversations_.push({ + query: this.query_ ?? "", + shouldDisplaySiteInfo: false, + title: this.siteInfo_.title ?? "", + url: this.siteInfo_.url ?? "", + response: "" + }); + this.query_ = ""; + this.$.promptInput.resetToAutoHeight(); + this.$.promptInput.focusInput(); + + this.isSubmittingQuery_ = true; + + const conversation_history: ConversationItem[] = []; + for (let i = this.conversations_.length - 1; i >= 0; i--) { + const conversation = this.conversations_[i]; + if (conversation != undefined && conversation.query.length > 0 && conversation.response.length > 0 && conversation_history.length <= 3) { + conversation_history.push({ + userQuery: conversation.query, + llmResponse: conversation.response, + }) + } + } + + setTimeout(() => + this.chatApiProxy_.submitQuery( + ActionType.QUERY, + this.submittedQuery_ ?? "", + this.shouldUseCurrentPageContentAsChatContext_ ? (this.siteInfo_.url || "") : "", conversation_history.reverse()), 0); + } + + protected onCancelQuery_() { + this.query_ = ""; + this.submittedQuery_ = ""; + this.isSubmittingQuery_ = false; + this.$.promptInput.resetToAutoHeight(); + this.$.promptInput.focusInput(); + setTimeout(() => this.chatApiProxy_.cancelQuery(), 0); + } + + protected openUrl_(url: string, modifiers: ClickModifiers) { + this.chatApiProxy_.openUrl(url, modifiers); + } + + refreshColorCss() { + const updater = ColorChangeUpdater.forDocument(); + updater.start(); + updater.refreshColorsCss(); + } + + override connectedCallback() { + super.connectedCallback(); + window.addEventListener('load', this.refreshColorCss); + setTimeout(async () => { + this.chatApiProxy_.showUI(); + const {siteInfo} = await this.chatApiProxy_.getSiteInfo(); + await this.updateSiteInfo(siteInfo); + }, 0); + + this.listenerIds_.push( + this.chatApiProxy_.getCallbackRouter().onSiteInfoChanged.addListener( + (siteInfo: SiteInfo) => this.updateSiteInfo(siteInfo)), + this.chatApiProxy_.getCallbackRouter().onSubmitActionResponse.addListener( + (response: ActionResponse) => this.updateSubmitResponse(response)) + ); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + + window.removeEventListener('load', this.refreshColorCss); + this.listenerIds_.forEach( + id => this.chatApiProxy_.getCallbackRouter().removeListener(id)); + } + +} + +declare global { + interface HTMLElementTagNameMap { + 'chat-app': ChatAppElement; + } +} + +customElements.define(ChatAppElement.is, ChatAppElement); \ No newline at end of file diff --git a/chrome/browser/resources/side_panel/chat/chat_prompt_input.css b/chrome/browser/resources/side_panel/chat/chat_prompt_input.css new file mode 100644 index 00000000000000..173a2aad293b6e --- /dev/null +++ b/chrome/browser/resources/side_panel/chat/chat_prompt_input.css @@ -0,0 +1,42 @@ +/* #css_wrapper_metadata_start + * #type=style-lit + * #import=../chat_prompt_input.css.js + * #scheme=relative + * #css_wrapper_metadata_end */ + +#input { + display: block; + resize: none; + line-height: 1.5em; + border: none; + outline: none; + padding: 0; + margin: 0; + font-weight: 400; + box-shadow: none; + height: 1lh; + min-height: 1lh; + font-family: 'Roboto', 'Arial', sans-serif; + width: 100%; + color: var(--color-yep-chat-primary-text); + background: var(--color-yep-chat-prompt-background); + border-radius: 0; + overflow-y: auto; +} + +#input:disabled { + cursor: wait; + opacity: 0.5; +} + +#input::-webkit-scrollbar { + width: 8px; + background-color: transparent; +} + +#input::-webkit-scrollbar-thumb { + background-color: var(--color-yep-chat-site-info-vertical-bar-background); + background-clip: content-box; + border: solid 1.5px transparent; + border-radius: 100px; +} diff --git a/chrome/browser/resources/side_panel/chat/chat_prompt_input.html.ts b/chrome/browser/resources/side_panel/chat/chat_prompt_input.html.ts new file mode 100644 index 00000000000000..8d18b72c574185 --- /dev/null +++ b/chrome/browser/resources/side_panel/chat/chat_prompt_input.html.ts @@ -0,0 +1,21 @@ +import {html, nothing} from '//resources/lit/v3_0/lit.rollup.js'; +import type {ChatPromptInputElement} from './chat_prompt_input.js'; + +export function getHtml(this: ChatPromptInputElement) { + return html` + `; +} diff --git a/chrome/browser/resources/side_panel/chat/chat_prompt_input.ts b/chrome/browser/resources/side_panel/chat/chat_prompt_input.ts new file mode 100644 index 00000000000000..836cf399290979 --- /dev/null +++ b/chrome/browser/resources/side_panel/chat/chat_prompt_input.ts @@ -0,0 +1,182 @@ +import type {PropertyValues} from '//resources/lit/v3_0/lit.rollup.js'; +import {CrLitElement} from '//resources/lit/v3_0/lit.rollup.js'; +import {getCss} from './chat_prompt_input.css.js'; +import {getHtml} from './chat_prompt_input.html.js'; + +export interface ChatPromptInputElement { + $: { + input: HTMLTextAreaElement, + }; +} + +export class ChatPromptInputElement extends CrLitElement { + static get is() { + return 'chat-prompt-input'; + } + + static override get styles() { + return getCss(); + } + + override render() { + return getHtml.bind(this)(); + } + + static override get properties() { + return { + autofocus: { + type: Boolean, + reflect: true, + }, + + disabled: { + type: Boolean, + reflect: true, + }, + + required: { + type: Boolean, + reflect: true, + }, + + maxlength: {type: Number}, + + readonly: { + type: Boolean, + reflect: true, + }, + + rows: { + type: Number, + reflect: true, + }, + + /** + * Text inside the text area. If the text exceeds the bounds of the text + * area, i.e. if it has more than |rows| lines, a scrollbar is shown by + * default when autogrow is not set. + */ + value: { + type: String, + notify: true, + }, + + internalValue_: { + type: String, + state: true, + }, + + placeholder: {type: String}, + }; + } + + override autofocus: boolean = false; + disabled: boolean = false; + readonly: boolean = false; + required: boolean = false; + rows: number = 1; + maxlength?: number; + value: string = ''; + placeholder: string = ''; + protected internalValue_: string = ''; + + override willUpdate(changedProperties: PropertyValues) { + super.willUpdate(changedProperties); + + if (changedProperties.has('value')) { + // Don't allow null or undefined as these will render in the input. + // cr-textarea cannot use Lit's "nothing" in the HTML template; this + // breaks the underlying native textarea's auto validation if |required| + // is set. + this.internalValue_ = + (this.value === undefined || this.value === null) ? '' : this.value; + } + } + + override updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if (changedProperties.has('disabled')) { + this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false'); + } + } + + focusInput() { + this.$.input.focus(); + } + + resetToAutoHeight() { + this.$.input.style.height = 'auto'; + } + + protected async onInputChange_(e: Event) { + // Ensure that |value| has been updated before re-firing 'change'. + await this.updateComplete; + this.dispatchEvent(new CustomEvent( + 'change', {bubbles: true, composed: true, detail: {sourceEvent: e}})); + } + + protected onKeydown_(e: KeyboardEvent) { + if (e.key === 'Enter') { + const textarea = this.$.input; + if (e.shiftKey) { + textarea.style.paddingTop = '14px'; + return; // Allow the default behavior + } else { + e.stopPropagation(); + e.preventDefault(); // Prevent adding a new line + const maxLength = this.maxlength === undefined ? Infinity : this.maxlength; + if (this.value.length <= maxLength) { + this.resetToAutoHeight(); + textarea.style.paddingTop = '0'; + this.fire('enter', {value: this.value}); + } + } + } + } + + // Display a vertical scrollbar when the content exceeds 10 lines + protected onInput_(e: Event) { + this.internalValue_ = (e.target as HTMLInputElement).value; + this.value = this.internalValue_; + + const textarea = this.$.input; + if (this.value === ""){ + textarea.style.paddingTop = '0'; + } + + const maxLines = 9; + textarea.style.height = '0'; + + const lineHeight = parseInt(window.getComputedStyle(textarea).lineHeight); + const paddingOffset = parseInt(window.getComputedStyle(textarea).paddingTop) + + parseInt(window.getComputedStyle(textarea).paddingBottom); + const contentHeight = textarea.scrollHeight - (lineHeight + paddingOffset); + const lines = Math.ceil(contentHeight / lineHeight); + + if (lines <= maxLines) { + textarea.style.height = `${lines * lineHeight}px`; + textarea.style.overflowY = 'hidden'; // No scrollbar for less than 10 lines + } else { + textarea.style.height = `${lineHeight * maxLines}px`; + textarea.style.overflowY = 'scroll'; // Show scrollbar after 10 lines + } + } + + protected onInputFocusChange_() { + // focused_ is used instead of :focus-within, so focus on elements within + // the suffix slot does not trigger a change in input styles. + if (this.shadowRoot!.activeElement === this.$.input) { + this.setAttribute('focused_', ''); + } else { + this.removeAttribute('focused_'); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + 'chat-prompt-input': ChatPromptInputElement; + } +} + +customElements.define(ChatPromptInputElement.is, ChatPromptInputElement); diff --git a/chrome/browser/resources/side_panel/chat/marked.d.ts b/chrome/browser/resources/side_panel/chat/marked.d.ts new file mode 100644 index 00000000000000..2c5b799ddeb09a --- /dev/null +++ b/chrome/browser/resources/side_panel/chat/marked.d.ts @@ -0,0 +1,727 @@ +// Generated by dts-bundle-generator v9.5.1 + +export type MarkedToken = (Tokens.Blockquote | Tokens.Br | Tokens.Code | Tokens.Codespan | Tokens.Def | Tokens.Del | Tokens.Em | Tokens.Escape | Tokens.Heading | Tokens.Hr | Tokens.HTML | Tokens.Image | Tokens.Link | Tokens.List | Tokens.ListItem | Tokens.Paragraph | Tokens.Space | Tokens.Strong | Tokens.Table | Tokens.Tag | Tokens.Text); +export type Token = (MarkedToken | Tokens.Generic); +export declare namespace Tokens { + interface Blockquote { + type: "blockquote"; + raw: string; + text: string; + tokens: Token[]; + } + interface Br { + type: "br"; + raw: string; + } + interface Checkbox { + checked: boolean; + } + interface Code { + type: "code"; + raw: string; + codeBlockStyle?: "indented"; + lang?: string; + text: string; + escaped?: boolean; + } + interface Codespan { + type: "codespan"; + raw: string; + text: string; + } + interface Def { + type: "def"; + raw: string; + tag: string; + href: string; + title: string; + } + interface Del { + type: "del"; + raw: string; + text: string; + tokens: Token[]; + } + interface Em { + type: "em"; + raw: string; + text: string; + tokens: Token[]; + } + interface Escape { + type: "escape"; + raw: string; + text: string; + } + interface Generic { + [index: string]: any; + type: string; + raw: string; + tokens?: Token[]; + } + interface Heading { + type: "heading"; + raw: string; + depth: number; + text: string; + tokens: Token[]; + } + interface Hr { + type: "hr"; + raw: string; + } + interface HTML { + type: "html"; + raw: string; + pre: boolean; + text: string; + block: boolean; + } + interface Image { + type: "image"; + raw: string; + href: string; + title: string | null; + text: string; + } + interface Link { + type: "link"; + raw: string; + href: string; + title?: string | null; + text: string; + tokens: Token[]; + } + interface List { + type: "list"; + raw: string; + ordered: boolean; + start: number | ""; + loose: boolean; + items: ListItem[]; + } + interface ListItem { + type: "list_item"; + raw: string; + task: boolean; + checked?: boolean; + loose: boolean; + text: string; + tokens: Token[]; + } + interface Paragraph { + type: "paragraph"; + raw: string; + pre?: boolean; + text: string; + tokens: Token[]; + } + interface Space { + type: "space"; + raw: string; + } + interface Strong { + type: "strong"; + raw: string; + text: string; + tokens: Token[]; + } + interface Table { + type: "table"; + raw: string; + align: Array<"center" | "left" | "right" | null>; + header: TableCell[]; + rows: TableCell[][]; + } + interface TableCell { + text: string; + tokens: Token[]; + header: boolean; + align: "center" | "left" | "right" | null; + } + interface TableRow { + text: string; + } + interface Tag { + type: "html"; + raw: string; + inLink: boolean; + inRawBlock: boolean; + text: string; + block: boolean; + } + interface Text { + type: "text"; + raw: string; + text: string; + tokens?: Token[]; + escaped?: boolean; + } +} +export type Links = Record>; +export type TokensList = Token[] & { + links: Links; +}; +/** + * Renderer + */ +declare class _Renderer { + options: MarkedOptions; + parser: _Parser; + constructor(options?: MarkedOptions); + space(token: Tokens.Space): string; + code({ text, lang, escaped }: Tokens.Code): string; + blockquote({ tokens }: Tokens.Blockquote): string; + html({ text }: Tokens.HTML | Tokens.Tag): string; + heading({ tokens, depth }: Tokens.Heading): string; + hr(token: Tokens.Hr): string; + list(token: Tokens.List): string; + listitem(item: Tokens.ListItem): string; + checkbox({ checked }: Tokens.Checkbox): string; + paragraph({ tokens }: Tokens.Paragraph): string; + table(token: Tokens.Table): string; + tablerow({ text }: Tokens.TableRow): string; + tablecell(token: Tokens.TableCell): string; + /** + * span level renderer + */ + strong({ tokens }: Tokens.Strong): string; + em({ tokens }: Tokens.Em): string; + codespan({ text }: Tokens.Codespan): string; + br(token: Tokens.Br): string; + del({ tokens }: Tokens.Del): string; + link({ href, title, tokens }: Tokens.Link): string; + image({ href, title, text }: Tokens.Image): string; + text(token: Tokens.Text | Tokens.Escape): string; +} +/** + * TextRenderer + * returns only the textual part of the token + */ +declare class _TextRenderer { + strong({ text }: Tokens.Strong): string; + em({ text }: Tokens.Em): string; + codespan({ text }: Tokens.Codespan): string; + del({ text }: Tokens.Del): string; + html({ text }: Tokens.HTML | Tokens.Tag): string; + text({ text }: Tokens.Text | Tokens.Escape | Tokens.Tag): string; + link({ text }: Tokens.Link): string; + image({ text }: Tokens.Image): string; + br(): string; +} +/** + * Parsing & Compiling + */ +declare class _Parser { + options: MarkedOptions; + renderer: _Renderer; + textRenderer: _TextRenderer; + constructor(options?: MarkedOptions); + /** + * Static Parse Method + */ + static parse(tokens: Token[], options?: MarkedOptions): string; + /** + * Static Parse Inline Method + */ + static parseInline(tokens: Token[], options?: MarkedOptions): string; + /** + * Parse Loop + */ + parse(tokens: Token[], top?: boolean): string; + /** + * Parse Inline Tokens + */ + parseInline(tokens: Token[], renderer?: _Renderer | _TextRenderer): string; +} +declare const other: { + codeRemoveIndent: RegExp; + outputLinkReplace: RegExp; + indentCodeCompensation: RegExp; + beginningSpace: RegExp; + endingHash: RegExp; + startingSpaceChar: RegExp; + endingSpaceChar: RegExp; + nonSpaceChar: RegExp; + newLineCharGlobal: RegExp; + tabCharGlobal: RegExp; + multipleSpaceGlobal: RegExp; + blankLine: RegExp; + doubleBlankLine: RegExp; + blockquoteStart: RegExp; + blockquoteSetextReplace: RegExp; + blockquoteSetextReplace2: RegExp; + listReplaceTabs: RegExp; + listReplaceNesting: RegExp; + listIsTask: RegExp; + listReplaceTask: RegExp; + anyLine: RegExp; + hrefBrackets: RegExp; + tableDelimiter: RegExp; + tableAlignChars: RegExp; + tableRowBlankLine: RegExp; + tableAlignRight: RegExp; + tableAlignCenter: RegExp; + tableAlignLeft: RegExp; + startATag: RegExp; + endATag: RegExp; + startPreScriptTag: RegExp; + endPreScriptTag: RegExp; + startAngleBracket: RegExp; + endAngleBracket: RegExp; + pedanticHrefTitle: RegExp; + unicodeAlphaNumeric: RegExp; + escapeTest: RegExp; + escapeReplace: RegExp; + escapeTestNoEncode: RegExp; + escapeReplaceNoEncode: RegExp; + unescapeTest: RegExp; + caret: RegExp; + percentDecode: RegExp; + findPipe: RegExp; + splitPipe: RegExp; + slashPipe: RegExp; + carriageReturn: RegExp; + spaceLine: RegExp; + notSpaceStart: RegExp; + endingNewline: RegExp; + listItemRegex: (bull: string) => RegExp; + nextBulletRegex: (indent: number) => RegExp; + hrRegex: (indent: number) => RegExp; + fencesBeginRegex: (indent: number) => RegExp; + headingBeginRegex: (indent: number) => RegExp; + htmlBeginRegex: (indent: number) => RegExp; +}; +declare const blockNormal: { + blockquote: RegExp; + code: RegExp; + def: RegExp; + fences: RegExp; + heading: RegExp; + hr: RegExp; + html: RegExp; + lheading: RegExp; + list: RegExp; + newline: RegExp; + paragraph: RegExp; + table: RegExp; + text: RegExp; +}; +export type BlockKeys = keyof typeof blockNormal; +declare const inlineNormal: { + _backpedal: RegExp; + anyPunctuation: RegExp; + autolink: RegExp; + blockSkip: RegExp; + br: RegExp; + code: RegExp; + del: RegExp; + emStrongLDelim: RegExp; + emStrongRDelimAst: RegExp; + emStrongRDelimUnd: RegExp; + escape: RegExp; + link: RegExp; + nolink: RegExp; + punctuation: RegExp; + reflink: RegExp; + reflinkSearch: RegExp; + tag: RegExp; + text: RegExp; + url: RegExp; +}; +export type InlineKeys = keyof typeof inlineNormal; +export interface Rules { + other: typeof other; + block: Record; + inline: Record; +} +/** + * Tokenizer + */ +declare class _Tokenizer { + options: MarkedOptions; + rules: Rules; + lexer: _Lexer; + constructor(options?: MarkedOptions); + space(src: string): Tokens.Space | undefined; + code(src: string): Tokens.Code | undefined; + fences(src: string): Tokens.Code | undefined; + heading(src: string): Tokens.Heading | undefined; + hr(src: string): Tokens.Hr | undefined; + blockquote(src: string): Tokens.Blockquote | undefined; + list(src: string): Tokens.List | undefined; + html(src: string): Tokens.HTML | undefined; + def(src: string): Tokens.Def | undefined; + table(src: string): Tokens.Table | undefined; + lheading(src: string): Tokens.Heading | undefined; + paragraph(src: string): Tokens.Paragraph | undefined; + text(src: string): Tokens.Text | undefined; + escape(src: string): Tokens.Escape | undefined; + tag(src: string): Tokens.Tag | undefined; + link(src: string): Tokens.Link | Tokens.Image | undefined; + reflink(src: string, links: Links): Tokens.Link | Tokens.Image | Tokens.Text | undefined; + emStrong(src: string, maskedSrc: string, prevChar?: string): Tokens.Em | Tokens.Strong | undefined; + codespan(src: string): Tokens.Codespan | undefined; + br(src: string): Tokens.Br | undefined; + del(src: string): Tokens.Del | undefined; + autolink(src: string): Tokens.Link | undefined; + url(src: string): Tokens.Link | undefined; + inlineText(src: string): Tokens.Text | undefined; +} +declare class _Hooks { + options: MarkedOptions; + block?: boolean; + constructor(options?: MarkedOptions); + static passThroughHooks: Set; + /** + * Process markdown before marked + */ + preprocess(markdown: string): string; + /** + * Process HTML after marked is finished + */ + postprocess(html: string): string; + /** + * Process all tokens before walk tokens + */ + processAllTokens(tokens: Token[] | TokensList): Token[] | TokensList; + /** + * Provide function to tokenize markdown + */ + provideLexer(): typeof _Lexer.lexInline; + /** + * Provide function to parse tokens + */ + provideParser(): typeof _Parser.parse; +} +export interface TokenizerThis { + lexer: _Lexer; +} +export type TokenizerExtensionFunction = (this: TokenizerThis, src: string, tokens: Token[] | TokensList) => Tokens.Generic | undefined; +export type TokenizerStartFunction = (this: TokenizerThis, src: string) => number | void; +export interface TokenizerExtension { + name: string; + level: "block" | "inline"; + start?: TokenizerStartFunction; + tokenizer: TokenizerExtensionFunction; + childTokens?: string[]; +} +export interface RendererThis { + parser: _Parser; +} +export type RendererExtensionFunction = (this: RendererThis, token: Tokens.Generic) => string | false | undefined; +export interface RendererExtension { + name: string; + renderer: RendererExtensionFunction; +} +export type TokenizerAndRendererExtension = TokenizerExtension | RendererExtension | (TokenizerExtension & RendererExtension); +export type HooksApi = Omit<_Hooks, "constructor" | "options" | "block">; +export type HooksObject = { + [K in keyof HooksApi]?: (this: _Hooks, ...args: Parameters) => ReturnType | Promise>; +}; +export type RendererApi = Omit<_Renderer, "constructor" | "options" | "parser">; +export type RendererObject = { + [K in keyof RendererApi]?: (this: _Renderer, ...args: Parameters) => ReturnType | false; +}; +export type TokenizerApi = Omit<_Tokenizer, "constructor" | "options" | "rules" | "lexer">; +export type TokenizerObject = { + [K in keyof TokenizerApi]?: (this: _Tokenizer, ...args: Parameters) => ReturnType | false; +}; +export interface MarkedExtension { + /** + * True will tell marked to await any walkTokens functions before parsing the tokens and returning an HTML string. + */ + async?: boolean; + /** + * Enable GFM line breaks. This option requires the gfm option to be true. + */ + breaks?: boolean; + /** + * Add tokenizers and renderers to marked + */ + extensions?: TokenizerAndRendererExtension[] | null; + /** + * Enable GitHub flavored markdown. + */ + gfm?: boolean; + /** + * Hooks are methods that hook into some part of marked. + * preprocess is called to process markdown before sending it to marked. + * processAllTokens is called with the TokensList before walkTokens. + * postprocess is called to process html after marked has finished parsing. + * provideLexer is called to provide a function to tokenize markdown. + * provideParser is called to provide a function to parse tokens. + */ + hooks?: HooksObject | null; + /** + * Conform to obscure parts of markdown.pl as much as possible. Don't fix any of the original markdown bugs or poor behavior. + */ + pedantic?: boolean; + /** + * Type: object Default: new Renderer() + * + * An object containing functions to render tokens to HTML. + */ + renderer?: RendererObject | null; + /** + * Shows an HTML error message when rendering fails. + */ + silent?: boolean; + /** + * The tokenizer defines how to turn markdown text into tokens. + */ + tokenizer?: TokenizerObject | null; + /** + * The walkTokens function gets called with every token. + * Child tokens are called before moving on to sibling tokens. + * Each token is passed by reference so updates are persisted when passed to the parser. + * The return value of the function is ignored. + */ + walkTokens?: ((token: Token) => void | Promise) | null; +} +export interface MarkedOptions extends Omit { + /** + * Hooks are methods that hook into some part of marked. + */ + hooks?: _Hooks | null; + /** + * Type: object Default: new Renderer() + * + * An object containing functions to render tokens to HTML. + */ + renderer?: _Renderer | null; + /** + * The tokenizer defines how to turn markdown text into tokens. + */ + tokenizer?: _Tokenizer | null; + /** + * Custom extensions + */ + extensions?: null | { + renderers: { + [name: string]: RendererExtensionFunction; + }; + childTokens: { + [name: string]: string[]; + }; + inline?: TokenizerExtensionFunction[]; + block?: TokenizerExtensionFunction[]; + startInline?: TokenizerStartFunction[]; + startBlock?: TokenizerStartFunction[]; + }; + /** + * walkTokens function returns array of values for Promise.all + */ + walkTokens?: null | ((token: Token) => void | Promise | (void | Promise)[]); +} +/** + * Block Lexer + */ +declare class _Lexer { + tokens: TokensList; + options: MarkedOptions; + state: { + inLink: boolean; + inRawBlock: boolean; + top: boolean; + }; + private tokenizer; + private inlineQueue; + constructor(options?: MarkedOptions); + /** + * Expose Rules + */ + static get rules(): { + block: { + normal: { + blockquote: RegExp; + code: RegExp; + def: RegExp; + fences: RegExp; + heading: RegExp; + hr: RegExp; + html: RegExp; + lheading: RegExp; + list: RegExp; + newline: RegExp; + paragraph: RegExp; + table: RegExp; + text: RegExp; + }; + gfm: Record<"code" | "blockquote" | "hr" | "html" | "table" | "text" | "def" | "heading" | "list" | "paragraph" | "fences" | "lheading" | "newline", RegExp>; + pedantic: Record<"code" | "blockquote" | "hr" | "html" | "table" | "text" | "def" | "heading" | "list" | "paragraph" | "fences" | "lheading" | "newline", RegExp>; + }; + inline: { + normal: { + _backpedal: RegExp; + anyPunctuation: RegExp; + autolink: RegExp; + blockSkip: RegExp; + br: RegExp; + code: RegExp; + del: RegExp; + emStrongLDelim: RegExp; + emStrongRDelimAst: RegExp; + emStrongRDelimUnd: RegExp; + escape: RegExp; + link: RegExp; + nolink: RegExp; + punctuation: RegExp; + reflink: RegExp; + reflinkSearch: RegExp; + tag: RegExp; + text: RegExp; + url: RegExp; + }; + gfm: Record<"link" | "code" | "url" | "br" | "del" | "text" | "escape" | "tag" | "reflink" | "nolink" | "_backpedal" | "anyPunctuation" | "autolink" | "blockSkip" | "emStrongLDelim" | "emStrongRDelimAst" | "emStrongRDelimUnd" | "punctuation" | "reflinkSearch", RegExp>; + breaks: Record<"link" | "code" | "url" | "br" | "del" | "text" | "escape" | "tag" | "reflink" | "nolink" | "_backpedal" | "anyPunctuation" | "autolink" | "blockSkip" | "emStrongLDelim" | "emStrongRDelimAst" | "emStrongRDelimUnd" | "punctuation" | "reflinkSearch", RegExp>; + pedantic: Record<"link" | "code" | "url" | "br" | "del" | "text" | "escape" | "tag" | "reflink" | "nolink" | "_backpedal" | "anyPunctuation" | "autolink" | "blockSkip" | "emStrongLDelim" | "emStrongRDelimAst" | "emStrongRDelimUnd" | "punctuation" | "reflinkSearch", RegExp>; + }; + }; + /** + * Static Lex Method + */ + static lex(src: string, options?: MarkedOptions): TokensList; + /** + * Static Lex Inline Method + */ + static lexInline(src: string, options?: MarkedOptions): Token[]; + /** + * Preprocessing + */ + lex(src: string): TokensList; + /** + * Lexing + */ + blockTokens(src: string, tokens?: Token[], lastParagraphClipped?: boolean): Token[]; + blockTokens(src: string, tokens?: TokensList, lastParagraphClipped?: boolean): TokensList; + inline(src: string, tokens?: Token[]): Token[]; + /** + * Lexing/Compiling + */ + inlineTokens(src: string, tokens?: Token[]): Token[]; +} +/** + * Gets the original marked default options. + */ +declare function _getDefaults(): MarkedOptions; +declare let _defaults: MarkedOptions; +export type MaybePromise = void | Promise; +export declare class Marked { + defaults: MarkedOptions; + options: (opt: MarkedOptions) => this; + parse: { + (src: string, options: MarkedOptions & { + async: true; + }): Promise; + (src: string, options: MarkedOptions & { + async: false; + }): string; + (src: string, options?: MarkedOptions | null): string | Promise; + }; + parseInline: { + (src: string, options: MarkedOptions & { + async: true; + }): Promise; + (src: string, options: MarkedOptions & { + async: false; + }): string; + (src: string, options?: MarkedOptions | null): string | Promise; + }; + Parser: typeof _Parser; + Renderer: typeof _Renderer; + TextRenderer: typeof _TextRenderer; + Lexer: typeof _Lexer; + Tokenizer: typeof _Tokenizer; + Hooks: typeof _Hooks; + constructor(...args: MarkedExtension[]); + /** + * Run callback for every token + */ + walkTokens(tokens: Token[] | TokensList, callback: (token: Token) => MaybePromise | MaybePromise[]): MaybePromise[]; + use(...args: MarkedExtension[]): this; + setOptions(opt: MarkedOptions): this; + lexer(src: string, options?: MarkedOptions): TokensList; + parser(tokens: Token[], options?: MarkedOptions): string; + private parseMarkdown; + private onError; +} +/** + * Compiles markdown to HTML asynchronously. + * + * @param src String of markdown source to be compiled + * @param options Hash of options, having async: true + * @return Promise of string of compiled HTML + */ +export declare function marked(src: string, options: MarkedOptions & { + async: true; +}): Promise; +/** + * Compiles markdown to HTML. + * + * @param src String of markdown source to be compiled + * @param options Optional hash of options + * @return String of compiled HTML. Will be a Promise of string if async is set to true by any extensions. + */ +export declare function marked(src: string, options: MarkedOptions & { + async: false; +}): string; +export declare function marked(src: string, options: MarkedOptions & { + async: true; +}): Promise; +export declare function marked(src: string, options?: MarkedOptions | null): string | Promise; +export declare namespace marked { + var options: (options: MarkedOptions) => typeof marked; + var setOptions: (options: MarkedOptions) => typeof marked; + var getDefaults: typeof _getDefaults; + var defaults: MarkedOptions; + var use: (...args: MarkedExtension[]) => typeof marked; + var walkTokens: (tokens: Token[] | TokensList, callback: (token: Token) => MaybePromise | MaybePromise[]) => MaybePromise[]; + var parseInline: { + (src: string, options: MarkedOptions & { + async: true; + }): Promise; + (src: string, options: MarkedOptions & { + async: false; + }): string; + (src: string, options?: MarkedOptions | null): string | Promise; + }; + var Parser: typeof _Parser; + var parser: typeof _Parser.parse; + var Renderer: typeof _Renderer; + var TextRenderer: typeof _TextRenderer; + var Lexer: typeof _Lexer; + var lexer: typeof _Lexer.lex; + var Tokenizer: typeof _Tokenizer; + var Hooks: typeof _Hooks; + var parse: typeof marked; +} +export declare const options: (options: MarkedOptions) => typeof marked; +export declare const setOptions: (options: MarkedOptions) => typeof marked; +export declare const use: (...args: MarkedExtension[]) => typeof marked; +export declare const walkTokens: (tokens: Token[] | TokensList, callback: (token: Token) => MaybePromise | MaybePromise[]) => MaybePromise[]; +export declare const parseInline: { + (src: string, options: MarkedOptions & { + async: true; + }): Promise; + (src: string, options: MarkedOptions & { + async: false; + }): string; + (src: string, options?: MarkedOptions | null): string | Promise; +}; +export declare const parse: typeof marked; +export declare const parser: typeof _Parser.parse; +export declare const lexer: typeof _Lexer.lex; + +export { + _Hooks as Hooks, + _Lexer as Lexer, + _Parser as Parser, + _Renderer as Renderer, + _TextRenderer as TextRenderer, + _Tokenizer as Tokenizer, + _defaults as defaults, + _getDefaults as getDefaults, +}; + +export {}; diff --git a/chrome/browser/resources/side_panel/chat/marked.ts b/chrome/browser/resources/side_panel/chat/marked.ts new file mode 100644 index 00000000000000..316ccd5a2f0d71 --- /dev/null +++ b/chrome/browser/resources/side_panel/chat/marked.ts @@ -0,0 +1,2556 @@ +// @ts-nocheck + +// Copy the code as-is and ensure proper attribution is provided at third_party/marked, +// following Chromium's recommended guidelines for crediting third-party code. + +/** marked v15.0.3 - a markdown parser + * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ + +/** + * DO NOT EDIT THIS FILE + * The code in this file is generated from files in ./src/ + */ + +/** + * Gets the original marked default options. + */ +function _getDefaults() { + return { + async: false, + breaks: false, + extensions: null, + gfm: true, + hooks: null, + pedantic: false, + renderer: null, + silent: false, + tokenizer: null, + walkTokens: null, + }; +} +let _defaults = _getDefaults(); +function changeDefaults(newDefaults) { + _defaults = newDefaults; +} + +const noopTest = { exec: () => null }; +function edit(regex, opt = '') { + let source = typeof regex === 'string' ? regex : regex.source; + const obj = { + replace: (name, val) => { + let valSource = typeof val === 'string' ? val : val.source; + valSource = valSource.replace(other.caret, '$1'); + source = source.replace(name, valSource); + return obj; + }, + getRegex: () => { + return new RegExp(source, opt); + }, + }; + return obj; +} +const other = { + codeRemoveIndent: /^(?: {1,4}| {0,3}\t)/gm, + outputLinkReplace: /\\([\[\]])/g, + indentCodeCompensation: /^(\s+)(?:```)/, + beginningSpace: /^\s+/, + endingHash: /#$/, + startingSpaceChar: /^ /, + endingSpaceChar: / $/, + nonSpaceChar: /[^ ]/, + newLineCharGlobal: /\n/g, + tabCharGlobal: /\t/g, + multipleSpaceGlobal: /\s+/g, + blankLine: /^[ \t]*$/, + doubleBlankLine: /\n[ \t]*\n[ \t]*$/, + blockquoteStart: /^ {0,3}>/, + blockquoteSetextReplace: /\n {0,3}((?:=+|-+) *)(?=\n|$)/g, + blockquoteSetextReplace2: /^ {0,3}>[ \t]?/gm, + listReplaceTabs: /^\t+/, + listReplaceNesting: /^ {1,4}(?=( {4})*[^ ])/g, + listIsTask: /^\[[ xX]\] /, + listReplaceTask: /^\[[ xX]\] +/, + anyLine: /\n.*\n/, + hrefBrackets: /^<(.*)>$/, + tableDelimiter: /[:|]/, + tableAlignChars: /^\||\| *$/g, + tableRowBlankLine: /\n[ \t]*$/, + tableAlignRight: /^ *-+: *$/, + tableAlignCenter: /^ *:-+: *$/, + tableAlignLeft: /^ *:-+ *$/, + startATag: /^/i, + startPreScriptTag: /^<(pre|code|kbd|script)(\s|>)/i, + endPreScriptTag: /^<\/(pre|code|kbd|script)(\s|>)/i, + startAngleBracket: /^$/, + pedanticHrefTitle: /^([^'"]*[^\s])\s+(['"])(.*)\2/, + unicodeAlphaNumeric: /[\p{L}\p{N}]/u, + escapeTest: /[&<>"']/, + escapeReplace: /[&<>"']/g, + escapeTestNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/, + escapeReplaceNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g, + unescapeTest: /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, + caret: /(^|[^\[])\^/g, + percentDecode: /%25/g, + findPipe: /\|/g, + splitPipe: / \|/, + slashPipe: /\\\|/g, + carriageReturn: /\r\n|\r/g, + spaceLine: /^ +$/gm, + notSpaceStart: /^\S*/, + endingNewline: /\n$/, + listItemRegex: (bull) => new RegExp(`^( {0,3}${bull})((?:[\t ][^\\n]*)?(?:\\n|$))`), + nextBulletRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`), + hrRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`), + fencesBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:\`\`\`|~~~)`), + headingBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}#`), + htmlBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}<(?:[a-z].*>|!--)`, 'i'), +}; +/** + * Block-Level Grammar + */ +const newline = /^(?:[ \t]*(?:\n|$))+/; +const blockCode = /^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/; +const fences = /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/; +const hr = /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/; +const heading = /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/; +const bullet = /(?:[*+-]|\d{1,9}[.)])/; +const lheading = edit(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/) + .replace(/bull/g, bullet) // lists can interrupt + .replace(/blockCode/g, /(?: {4}| {0,3}\t)/) // indented code blocks can interrupt + .replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/) // fenced code blocks can interrupt + .replace(/blockquote/g, / {0,3}>/) // blockquote can interrupt + .replace(/heading/g, / {0,3}#{1,6}/) // ATX heading can interrupt + .replace(/html/g, / {0,3}<[^\n>]+>\n/) // block html can interrupt + .getRegex(); +const _paragraph = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/; +const blockText = /^[^\n]+/; +const _blockLabel = /(?!\s*\])(?:\\.|[^\[\]\\])+/; +const def = edit(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/) + .replace('label', _blockLabel) + .replace('title', /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/) + .getRegex(); +const list = edit(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/) + .replace(/bull/g, bullet) + .getRegex(); +const _tag = 'address|article|aside|base|basefont|blockquote|body|caption' + + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption' + + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe' + + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option' + + '|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title' + + '|tr|track|ul'; +const _comment = /|$))/; +const html = edit('^ {0,3}(?:' // optional indentation + + '<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)' // (1) + + '|comment[^\\n]*(\\n+|$)' // (2) + + '|<\\?[\\s\\S]*?(?:\\?>\\n*|$)' // (3) + + '|\\n*|$)' // (4) + + '|\\n*|$)' // (5) + + '|)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (6) + + '|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (7) open tag + + '|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (7) closing tag + + ')', 'i') + .replace('comment', _comment) + .replace('tag', _tag) + .replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/) + .getRegex(); +const paragraph = edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs + .replace('|table', '') + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // pars can be interrupted by type (6) html blocks + .getRegex(); +const blockquote = edit(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/) + .replace('paragraph', paragraph) + .getRegex(); +/** + * Normal Block Grammar + */ +const blockNormal = { + blockquote, + code: blockCode, + def, + fences, + heading, + hr, + html, + lheading, + list, + newline, + paragraph, + table: noopTest, + text: blockText, +}; +/** + * GFM Block Grammar + */ +const gfmTable = edit('^ *([^\\n ].*)\\n' // Header + + ' {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)' // Align + + '(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)') // Cells + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('blockquote', ' {0,3}>') + .replace('code', '(?: {4}| {0,3}\t)[^\\n]') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // tables can be interrupted by type (6) html blocks + .getRegex(); +const blockGfm = { + ...blockNormal, + table: gfmTable, + paragraph: edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs + .replace('table', gfmTable) // interrupt paragraphs with table + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // pars can be interrupted by type (6) html blocks + .getRegex(), +}; +/** + * Pedantic grammar (original John Gruber's loose markdown specification) + */ +const blockPedantic = { + ...blockNormal, + html: edit('^ *(?:comment *(?:\\n|\\s*$)' + + '|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)' // closed tag + + '|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))') + .replace('comment', _comment) + .replace(/tag/g, '(?!(?:' + + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub' + + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)' + + '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b') + .getRegex(), + def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, + heading: /^(#{1,6})(.*)(?:\n+|$)/, + fences: noopTest, // fences not supported + lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, + paragraph: edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' *#{1,6} *[^\n]') + .replace('lheading', lheading) + .replace('|table', '') + .replace('blockquote', ' {0,3}>') + .replace('|fences', '') + .replace('|list', '') + .replace('|html', '') + .replace('|tag', '') + .getRegex(), +}; +/** + * Inline-Level Grammar + */ +const escape$1 = /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/; +const inlineCode = /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/; +const br = /^( {2,}|\\)\n(?!\s*$)/; +const inlineText = /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\ +const blockSkip = /\[[^[\]]*?\]\((?:\\.|[^\\\(\)]|\((?:\\.|[^\\\(\)])*\))*\)|`[^`]*?`|<[^<>]*?>/g; +const emStrongLDelim = edit(/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/, 'u') + .replace(/punct/g, _punctuation) + .getRegex(); +const emStrongRDelimAst = edit('^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)' // Skip orphan inside strong + + '|[^*]+(?=[^*])' // Consume to delim + + '|(?!\\*)punct(\\*+)(?=[\\s]|$)' // (1) #*** can only be a Right Delimiter + + '|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)' // (2) a***#, a*** can only be a Right Delimiter + + '|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)' // (3) #***a, ***a can only be Left Delimiter + + '|[\\s](\\*+)(?!\\*)(?=punct)' // (4) ***# can only be Left Delimiter + + '|(?!\\*)punct(\\*+)(?!\\*)(?=punct)' // (5) #***# can be either Left or Right Delimiter + + '|notPunctSpace(\\*+)(?=notPunctSpace)', 'gu') // (6) a***a can be either Left or Right Delimiter + .replace(/notPunctSpace/g, _notPunctuationOrSpace) + .replace(/punctSpace/g, _punctuationOrSpace) + .replace(/punct/g, _punctuation) + .getRegex(); +// (6) Not allowed for _ +const emStrongRDelimUnd = edit('^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)' // Skip orphan inside strong + + '|[^_]+(?=[^_])' // Consume to delim + + '|(?!_)punct(_+)(?=[\\s]|$)' // (1) #___ can only be a Right Delimiter + + '|notPunctSpace(_+)(?!_)(?=punctSpace|$)' // (2) a___#, a___ can only be a Right Delimiter + + '|(?!_)punctSpace(_+)(?=notPunctSpace)' // (3) #___a, ___a can only be Left Delimiter + + '|[\\s](_+)(?!_)(?=punct)' // (4) ___# can only be Left Delimiter + + '|(?!_)punct(_+)(?!_)(?=punct)', 'gu') // (5) #___# can be either Left or Right Delimiter + .replace(/notPunctSpace/g, _notPunctuationOrSpace) + .replace(/punctSpace/g, _punctuationOrSpace) + .replace(/punct/g, _punctuation) + .getRegex(); +const anyPunctuation = edit(/\\(punct)/, 'gu') + .replace(/punct/g, _punctuation) + .getRegex(); +const autolink = edit(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/) + .replace('scheme', /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/) + .replace('email', /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/) + .getRegex(); +const _inlineComment = edit(_comment).replace('(?:-->|$)', '-->').getRegex(); +const tag = edit('^comment' + + '|^' // self-closing tag + + '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag + + '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. + + '|^' // declaration, e.g. + + '|^') // CDATA section + .replace('comment', _inlineComment) + .replace('attribute', /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/) + .getRegex(); +const _inlineLabel = /(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/; +const link = edit(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/) + .replace('label', _inlineLabel) + .replace('href', /<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/) + .replace('title', /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/) + .getRegex(); +const reflink = edit(/^!?\[(label)\]\[(ref)\]/) + .replace('label', _inlineLabel) + .replace('ref', _blockLabel) + .getRegex(); +const nolink = edit(/^!?\[(ref)\](?:\[\])?/) + .replace('ref', _blockLabel) + .getRegex(); +const reflinkSearch = edit('reflink|nolink(?!\\()', 'g') + .replace('reflink', reflink) + .replace('nolink', nolink) + .getRegex(); +/** + * Normal Inline Grammar + */ +const inlineNormal = { + _backpedal: noopTest, // only used for GFM url + anyPunctuation, + autolink, + blockSkip, + br, + code: inlineCode, + del: noopTest, + emStrongLDelim, + emStrongRDelimAst, + emStrongRDelimUnd, + escape: escape$1, + link, + nolink, + punctuation, + reflink, + reflinkSearch, + tag, + text: inlineText, + url: noopTest, +}; +/** + * Pedantic Inline Grammar + */ +const inlinePedantic = { + ...inlineNormal, + link: edit(/^!?\[(label)\]\((.*?)\)/) + .replace('label', _inlineLabel) + .getRegex(), + reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/) + .replace('label', _inlineLabel) + .getRegex(), +}; +/** + * GFM Inline Grammar + */ +const inlineGfm = { + ...inlineNormal, + escape: edit(escape$1).replace('])', '~|])').getRegex(), + url: edit(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/, 'i') + .replace('email', /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/) + .getRegex(), + _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, + del: /^(~~?)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/, + text: /^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\': '>', + '"': '"', + "'": ''', +}; +const getEscapeReplacement = (ch) => escapeReplacements[ch]; +function escape(html, encode) { + if (encode) { + if (other.escapeTest.test(html)) { + return html.replace(other.escapeReplace, getEscapeReplacement); + } + } + else { + if (other.escapeTestNoEncode.test(html)) { + return html.replace(other.escapeReplaceNoEncode, getEscapeReplacement); + } + } + return html; +} +function cleanUrl(href) { + try { + href = encodeURI(href).replace(other.percentDecode, '%'); + } + catch { + return null; + } + return href; +} +function splitCells(tableRow, count) { + // ensure that every cell-delimiting pipe has a space + // before it to distinguish it from an escaped pipe + const row = tableRow.replace(other.findPipe, (match, offset, str) => { + let escaped = false; + let curr = offset; + while (--curr >= 0 && str[curr] === '\\') + escaped = !escaped; + if (escaped) { + // odd number of slashes means | is escaped + // so we leave it alone + return '|'; + } + else { + // add space before unescaped | + return ' |'; + } + }), cells = row.split(other.splitPipe); + let i = 0; + // First/last cell in a row cannot be empty if it has no leading/trailing pipe + if (!cells[0].trim()) { + cells.shift(); + } + if (cells.length > 0 && !cells.at(-1)?.trim()) { + cells.pop(); + } + if (count) { + if (cells.length > count) { + cells.splice(count); + } + else { + while (cells.length < count) + cells.push(''); + } + } + for (; i < cells.length; i++) { + // leading or trailing whitespace is ignored per the gfm spec + cells[i] = cells[i].trim().replace(other.slashPipe, '|'); + } + return cells; +} +/** + * Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). + * /c*$/ is vulnerable to REDOS. + * + * @param str + * @param c + * @param invert Remove suffix of non-c chars instead. Default falsey. + */ +function rtrim(str, c, invert) { + const l = str.length; + if (l === 0) { + return ''; + } + // Length of suffix matching the invert condition. + let suffLen = 0; + // Step left until we fail to match the invert condition. + while (suffLen < l) { + const currChar = str.charAt(l - suffLen - 1); + if (currChar === c && !invert) { + suffLen++; + } + else if (currChar !== c && invert) { + suffLen++; + } + else { + break; + } + } + return str.slice(0, l - suffLen); +} +function findClosingBracket(str, b) { + if (str.indexOf(b[1]) === -1) { + return -1; + } + let level = 0; + for (let i = 0; i < str.length; i++) { + if (str[i] === '\\') { + i++; + } + else if (str[i] === b[0]) { + level++; + } + else if (str[i] === b[1]) { + level--; + if (level < 0) { + return i; + } + } + } + return -1; +} + +function outputLink(cap, link, raw, lexer, rules) { + const href = link.href; + const title = link.title || null; + const text = cap[1].replace(rules.other.outputLinkReplace, '$1'); + if (cap[0].charAt(0) !== '!') { + lexer.state.inLink = true; + const token = { + type: 'link', + raw, + href, + title, + text, + tokens: lexer.inlineTokens(text), + }; + lexer.state.inLink = false; + return token; + } + return { + type: 'image', + raw, + href, + title, + text, + }; +} +function indentCodeCompensation(raw, text, rules) { + const matchIndentToCode = raw.match(rules.other.indentCodeCompensation); + if (matchIndentToCode === null) { + return text; + } + const indentToCode = matchIndentToCode[1]; + return text + .split('\n') + .map(node => { + const matchIndentInNode = node.match(rules.other.beginningSpace); + if (matchIndentInNode === null) { + return node; + } + const [indentInNode] = matchIndentInNode; + if (indentInNode.length >= indentToCode.length) { + return node.slice(indentToCode.length); + } + return node; + }) + .join('\n'); +} +/** + * Tokenizer + */ +class _Tokenizer { + options; + rules; // set by the lexer + lexer; // set by the lexer + constructor(options) { + this.options = options || _defaults; + } + space(src) { + const cap = this.rules.block.newline.exec(src); + if (cap && cap[0].length > 0) { + return { + type: 'space', + raw: cap[0], + }; + } + } + code(src) { + const cap = this.rules.block.code.exec(src); + if (cap) { + const text = cap[0].replace(this.rules.other.codeRemoveIndent, ''); + return { + type: 'code', + raw: cap[0], + codeBlockStyle: 'indented', + text: !this.options.pedantic + ? rtrim(text, '\n') + : text, + }; + } + } + fences(src) { + const cap = this.rules.block.fences.exec(src); + if (cap) { + const raw = cap[0]; + const text = indentCodeCompensation(raw, cap[3] || '', this.rules); + return { + type: 'code', + raw, + lang: cap[2] ? cap[2].trim().replace(this.rules.inline.anyPunctuation, '$1') : cap[2], + text, + }; + } + } + heading(src) { + const cap = this.rules.block.heading.exec(src); + if (cap) { + let text = cap[2].trim(); + // remove trailing #s + if (this.rules.other.endingHash.test(text)) { + const trimmed = rtrim(text, '#'); + if (this.options.pedantic) { + text = trimmed.trim(); + } + else if (!trimmed || this.rules.other.endingSpaceChar.test(trimmed)) { + // CommonMark requires space before trailing #s + text = trimmed.trim(); + } + } + return { + type: 'heading', + raw: cap[0], + depth: cap[1].length, + text, + tokens: this.lexer.inline(text), + }; + } + } + hr(src) { + const cap = this.rules.block.hr.exec(src); + if (cap) { + return { + type: 'hr', + raw: rtrim(cap[0], '\n'), + }; + } + } + blockquote(src) { + const cap = this.rules.block.blockquote.exec(src); + if (cap) { + let lines = rtrim(cap[0], '\n').split('\n'); + let raw = ''; + let text = ''; + const tokens = []; + while (lines.length > 0) { + let inBlockquote = false; + const currentLines = []; + let i; + for (i = 0; i < lines.length; i++) { + // get lines up to a continuation + if (this.rules.other.blockquoteStart.test(lines[i])) { + currentLines.push(lines[i]); + inBlockquote = true; + } + else if (!inBlockquote) { + currentLines.push(lines[i]); + } + else { + break; + } + } + lines = lines.slice(i); + const currentRaw = currentLines.join('\n'); + const currentText = currentRaw + // precede setext continuation with 4 spaces so it isn't a setext + .replace(this.rules.other.blockquoteSetextReplace, '\n $1') + .replace(this.rules.other.blockquoteSetextReplace2, ''); + raw = raw ? `${raw}\n${currentRaw}` : currentRaw; + text = text ? `${text}\n${currentText}` : currentText; + // parse blockquote lines as top level tokens + // merge paragraphs if this is a continuation + const top = this.lexer.state.top; + this.lexer.state.top = true; + this.lexer.blockTokens(currentText, tokens, true); + this.lexer.state.top = top; + // if there is no continuation then we are done + if (lines.length === 0) { + break; + } + const lastToken = tokens.at(-1); + if (lastToken?.type === 'code') { + // blockquote continuation cannot be preceded by a code block + break; + } + else if (lastToken?.type === 'blockquote') { + // include continuation in nested blockquote + const oldToken = lastToken; + const newText = oldToken.raw + '\n' + lines.join('\n'); + const newToken = this.blockquote(newText); + tokens[tokens.length - 1] = newToken; + raw = raw.substring(0, raw.length - oldToken.raw.length) + newToken.raw; + text = text.substring(0, text.length - oldToken.text.length) + newToken.text; + break; + } + else if (lastToken?.type === 'list') { + // include continuation in nested list + const oldToken = lastToken; + const newText = oldToken.raw + '\n' + lines.join('\n'); + const newToken = this.list(newText); + tokens[tokens.length - 1] = newToken; + raw = raw.substring(0, raw.length - lastToken.raw.length) + newToken.raw; + text = text.substring(0, text.length - oldToken.raw.length) + newToken.raw; + lines = newText.substring(tokens.at(-1).raw.length).split('\n'); + continue; + } + } + return { + type: 'blockquote', + raw, + tokens, + text, + }; + } + } + list(src) { + let cap = this.rules.block.list.exec(src); + if (cap) { + let bull = cap[1].trim(); + const isordered = bull.length > 1; + const list = { + type: 'list', + raw: '', + ordered: isordered, + start: isordered ? +bull.slice(0, -1) : '', + loose: false, + items: [], + }; + bull = isordered ? `\\d{1,9}\\${bull.slice(-1)}` : `\\${bull}`; + if (this.options.pedantic) { + bull = isordered ? bull : '[*+-]'; + } + // Get next list item + const itemRegex = this.rules.other.listItemRegex(bull); + let endsWithBlankLine = false; + // Check if current bullet point can start a new List Item + while (src) { + let endEarly = false; + let raw = ''; + let itemContents = ''; + if (!(cap = itemRegex.exec(src))) { + break; + } + if (this.rules.block.hr.test(src)) { // End list if bullet was actually HR (possibly move into itemRegex?) + break; + } + raw = cap[0]; + src = src.substring(raw.length); + let line = cap[2].split('\n', 1)[0].replace(this.rules.other.listReplaceTabs, (t) => ' '.repeat(3 * t.length)); + let nextLine = src.split('\n', 1)[0]; + let blankLine = !line.trim(); + let indent = 0; + if (this.options.pedantic) { + indent = 2; + itemContents = line.trimStart(); + } + else if (blankLine) { + indent = cap[1].length + 1; + } + else { + indent = cap[2].search(this.rules.other.nonSpaceChar); // Find first non-space char + indent = indent > 4 ? 1 : indent; // Treat indented code blocks (> 4 spaces) as having only 1 indent + itemContents = line.slice(indent); + indent += cap[1].length; + } + if (blankLine && this.rules.other.blankLine.test(nextLine)) { // Items begin with at most one blank line + raw += nextLine + '\n'; + src = src.substring(nextLine.length + 1); + endEarly = true; + } + if (!endEarly) { + const nextBulletRegex = this.rules.other.nextBulletRegex(indent); + const hrRegex = this.rules.other.hrRegex(indent); + const fencesBeginRegex = this.rules.other.fencesBeginRegex(indent); + const headingBeginRegex = this.rules.other.headingBeginRegex(indent); + const htmlBeginRegex = this.rules.other.htmlBeginRegex(indent); + // Check if following lines should be included in List Item + while (src) { + const rawLine = src.split('\n', 1)[0]; + let nextLineWithoutTabs; + nextLine = rawLine; + // Re-align to follow commonmark nesting rules + if (this.options.pedantic) { + nextLine = nextLine.replace(this.rules.other.listReplaceNesting, ' '); + nextLineWithoutTabs = nextLine; + } + else { + nextLineWithoutTabs = nextLine.replace(this.rules.other.tabCharGlobal, ' '); + } + // End list item if found code fences + if (fencesBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of new heading + if (headingBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of html block + if (htmlBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of new bullet + if (nextBulletRegex.test(nextLine)) { + break; + } + // Horizontal rule found + if (hrRegex.test(nextLine)) { + break; + } + if (nextLineWithoutTabs.search(this.rules.other.nonSpaceChar) >= indent || !nextLine.trim()) { // Dedent if possible + itemContents += '\n' + nextLineWithoutTabs.slice(indent); + } + else { + // not enough indentation + if (blankLine) { + break; + } + // paragraph continuation unless last line was a different block level element + if (line.replace(this.rules.other.tabCharGlobal, ' ').search(this.rules.other.nonSpaceChar) >= 4) { // indented code block + break; + } + if (fencesBeginRegex.test(line)) { + break; + } + if (headingBeginRegex.test(line)) { + break; + } + if (hrRegex.test(line)) { + break; + } + itemContents += '\n' + nextLine; + } + if (!blankLine && !nextLine.trim()) { // Check if current line is blank + blankLine = true; + } + raw += rawLine + '\n'; + src = src.substring(rawLine.length + 1); + line = nextLineWithoutTabs.slice(indent); + } + } + if (!list.loose) { + // If the previous item ended with a blank line, the list is loose + if (endsWithBlankLine) { + list.loose = true; + } + else if (this.rules.other.doubleBlankLine.test(raw)) { + endsWithBlankLine = true; + } + } + let istask = null; + let ischecked; + // Check for task list items + if (this.options.gfm) { + istask = this.rules.other.listIsTask.exec(itemContents); + if (istask) { + ischecked = istask[0] !== '[ ] '; + itemContents = itemContents.replace(this.rules.other.listReplaceTask, ''); + } + } + list.items.push({ + type: 'list_item', + raw, + task: !!istask, + checked: ischecked, + loose: false, + text: itemContents, + tokens: [], + }); + list.raw += raw; + } + // Do not consume newlines at end of final item. Alternatively, make itemRegex *start* with any newlines to simplify/speed up endsWithBlankLine logic + const lastItem = list.items.at(-1); + if (lastItem) { + lastItem.raw = lastItem.raw.trimEnd(); + lastItem.text = lastItem.text.trimEnd(); + } + list.raw = list.raw.trimEnd(); + // Item child tokens handled here at end because we needed to have the final item to trim it first + for (let i = 0; i < list.items.length; i++) { + this.lexer.state.top = false; + list.items[i].tokens = this.lexer.blockTokens(list.items[i].text, []); + if (!list.loose) { + // Check if list should be loose + const spacers = list.items[i].tokens.filter(t => t.type === 'space'); + const hasMultipleLineBreaks = spacers.length > 0 && spacers.some(t => this.rules.other.anyLine.test(t.raw)); + list.loose = hasMultipleLineBreaks; + } + } + // Set all items to loose if list is loose + if (list.loose) { + for (let i = 0; i < list.items.length; i++) { + list.items[i].loose = true; + } + } + return list; + } + } + html(src) { + const cap = this.rules.block.html.exec(src); + if (cap) { + const token = { + type: 'html', + block: true, + raw: cap[0], + pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style', + text: cap[0], + }; + return token; + } + } + def(src) { + const cap = this.rules.block.def.exec(src); + if (cap) { + const tag = cap[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal, ' '); + const href = cap[2] ? cap[2].replace(this.rules.other.hrefBrackets, '$1').replace(this.rules.inline.anyPunctuation, '$1') : ''; + const title = cap[3] ? cap[3].substring(1, cap[3].length - 1).replace(this.rules.inline.anyPunctuation, '$1') : cap[3]; + return { + type: 'def', + tag, + raw: cap[0], + href, + title, + }; + } + } + table(src) { + const cap = this.rules.block.table.exec(src); + if (!cap) { + return; + } + if (!this.rules.other.tableDelimiter.test(cap[2])) { + // delimiter row must have a pipe (|) or colon (:) otherwise it is a setext heading + return; + } + const headers = splitCells(cap[1]); + const aligns = cap[2].replace(this.rules.other.tableAlignChars, '').split('|'); + const rows = cap[3]?.trim() ? cap[3].replace(this.rules.other.tableRowBlankLine, '').split('\n') : []; + const item = { + type: 'table', + raw: cap[0], + header: [], + align: [], + rows: [], + }; + if (headers.length !== aligns.length) { + // header and align columns must be equal, rows can be different. + return; + } + for (const align of aligns) { + if (this.rules.other.tableAlignRight.test(align)) { + item.align.push('right'); + } + else if (this.rules.other.tableAlignCenter.test(align)) { + item.align.push('center'); + } + else if (this.rules.other.tableAlignLeft.test(align)) { + item.align.push('left'); + } + else { + item.align.push(null); + } + } + for (let i = 0; i < headers.length; i++) { + item.header.push({ + text: headers[i], + tokens: this.lexer.inline(headers[i]), + header: true, + align: item.align[i], + }); + } + for (const row of rows) { + item.rows.push(splitCells(row, item.header.length).map((cell, i) => { + return { + text: cell, + tokens: this.lexer.inline(cell), + header: false, + align: item.align[i], + }; + })); + } + return item; + } + lheading(src) { + const cap = this.rules.block.lheading.exec(src); + if (cap) { + return { + type: 'heading', + raw: cap[0], + depth: cap[2].charAt(0) === '=' ? 1 : 2, + text: cap[1], + tokens: this.lexer.inline(cap[1]), + }; + } + } + paragraph(src) { + const cap = this.rules.block.paragraph.exec(src); + if (cap) { + const text = cap[1].charAt(cap[1].length - 1) === '\n' + ? cap[1].slice(0, -1) + : cap[1]; + return { + type: 'paragraph', + raw: cap[0], + text, + tokens: this.lexer.inline(text), + }; + } + } + text(src) { + const cap = this.rules.block.text.exec(src); + if (cap) { + return { + type: 'text', + raw: cap[0], + text: cap[0], + tokens: this.lexer.inline(cap[0]), + }; + } + } + escape(src) { + const cap = this.rules.inline.escape.exec(src); + if (cap) { + return { + type: 'escape', + raw: cap[0], + text: cap[1], + }; + } + } + tag(src) { + const cap = this.rules.inline.tag.exec(src); + if (cap) { + if (!this.lexer.state.inLink && this.rules.other.startATag.test(cap[0])) { + this.lexer.state.inLink = true; + } + else if (this.lexer.state.inLink && this.rules.other.endATag.test(cap[0])) { + this.lexer.state.inLink = false; + } + if (!this.lexer.state.inRawBlock && this.rules.other.startPreScriptTag.test(cap[0])) { + this.lexer.state.inRawBlock = true; + } + else if (this.lexer.state.inRawBlock && this.rules.other.endPreScriptTag.test(cap[0])) { + this.lexer.state.inRawBlock = false; + } + return { + type: 'html', + raw: cap[0], + inLink: this.lexer.state.inLink, + inRawBlock: this.lexer.state.inRawBlock, + block: false, + text: cap[0], + }; + } + } + link(src) { + const cap = this.rules.inline.link.exec(src); + if (cap) { + const trimmedUrl = cap[2].trim(); + if (!this.options.pedantic && this.rules.other.startAngleBracket.test(trimmedUrl)) { + // commonmark requires matching angle brackets + if (!(this.rules.other.endAngleBracket.test(trimmedUrl))) { + return; + } + // ending angle bracket cannot be escaped + const rtrimSlash = rtrim(trimmedUrl.slice(0, -1), '\\'); + if ((trimmedUrl.length - rtrimSlash.length) % 2 === 0) { + return; + } + } + else { + // find closing parenthesis + const lastParenIndex = findClosingBracket(cap[2], '()'); + if (lastParenIndex > -1) { + const start = cap[0].indexOf('!') === 0 ? 5 : 4; + const linkLen = start + cap[1].length + lastParenIndex; + cap[2] = cap[2].substring(0, lastParenIndex); + cap[0] = cap[0].substring(0, linkLen).trim(); + cap[3] = ''; + } + } + let href = cap[2]; + let title = ''; + if (this.options.pedantic) { + // split pedantic href and title + const link = this.rules.other.pedanticHrefTitle.exec(href); + if (link) { + href = link[1]; + title = link[3]; + } + } + else { + title = cap[3] ? cap[3].slice(1, -1) : ''; + } + href = href.trim(); + if (this.rules.other.startAngleBracket.test(href)) { + if (this.options.pedantic && !(this.rules.other.endAngleBracket.test(trimmedUrl))) { + // pedantic allows starting angle bracket without ending angle bracket + href = href.slice(1); + } + else { + href = href.slice(1, -1); + } + } + return outputLink(cap, { + href: href ? href.replace(this.rules.inline.anyPunctuation, '$1') : href, + title: title ? title.replace(this.rules.inline.anyPunctuation, '$1') : title, + }, cap[0], this.lexer, this.rules); + } + } + reflink(src, links) { + let cap; + if ((cap = this.rules.inline.reflink.exec(src)) + || (cap = this.rules.inline.nolink.exec(src))) { + const linkString = (cap[2] || cap[1]).replace(this.rules.other.multipleSpaceGlobal, ' '); + const link = links[linkString.toLowerCase()]; + if (!link) { + const text = cap[0].charAt(0); + return { + type: 'text', + raw: text, + text, + }; + } + return outputLink(cap, link, cap[0], this.lexer, this.rules); + } + } + emStrong(src, maskedSrc, prevChar = '') { + let match = this.rules.inline.emStrongLDelim.exec(src); + if (!match) + return; + // _ can't be between two alphanumerics. \p{L}\p{N} includes non-english alphabet/numbers as well + if (match[3] && prevChar.match(this.rules.other.unicodeAlphaNumeric)) + return; + const nextChar = match[1] || match[2] || ''; + if (!nextChar || !prevChar || this.rules.inline.punctuation.exec(prevChar)) { + // unicode Regex counts emoji as 1 char; spread into array for proper count (used multiple times below) + const lLength = [...match[0]].length - 1; + let rDelim, rLength, delimTotal = lLength, midDelimTotal = 0; + const endReg = match[0][0] === '*' ? this.rules.inline.emStrongRDelimAst : this.rules.inline.emStrongRDelimUnd; + endReg.lastIndex = 0; + // Clip maskedSrc to same section of string as src (move to lexer?) + maskedSrc = maskedSrc.slice(-1 * src.length + lLength); + while ((match = endReg.exec(maskedSrc)) != null) { + rDelim = match[1] || match[2] || match[3] || match[4] || match[5] || match[6]; + if (!rDelim) + continue; // skip single * in __abc*abc__ + rLength = [...rDelim].length; + if (match[3] || match[4]) { // found another Left Delim + delimTotal += rLength; + continue; + } + else if (match[5] || match[6]) { // either Left or Right Delim + if (lLength % 3 && !((lLength + rLength) % 3)) { + midDelimTotal += rLength; + continue; // CommonMark Emphasis Rules 9-10 + } + } + delimTotal -= rLength; + if (delimTotal > 0) + continue; // Haven't found enough closing delimiters + // Remove extra characters. *a*** -> *a* + rLength = Math.min(rLength, rLength + delimTotal + midDelimTotal); + // char length can be >1 for unicode characters; + const lastCharLength = [...match[0]][0].length; + const raw = src.slice(0, lLength + match.index + lastCharLength + rLength); + // Create `em` if smallest delimiter has odd char count. *a*** + if (Math.min(lLength, rLength) % 2) { + const text = raw.slice(1, -1); + return { + type: 'em', + raw, + text, + tokens: this.lexer.inlineTokens(text), + }; + } + // Create 'strong' if smallest delimiter has even char count. **a*** + const text = raw.slice(2, -2); + return { + type: 'strong', + raw, + text, + tokens: this.lexer.inlineTokens(text), + }; + } + } + } + codespan(src) { + const cap = this.rules.inline.code.exec(src); + if (cap) { + let text = cap[2].replace(this.rules.other.newLineCharGlobal, ' '); + const hasNonSpaceChars = this.rules.other.nonSpaceChar.test(text); + const hasSpaceCharsOnBothEnds = this.rules.other.startingSpaceChar.test(text) && this.rules.other.endingSpaceChar.test(text); + if (hasNonSpaceChars && hasSpaceCharsOnBothEnds) { + text = text.substring(1, text.length - 1); + } + return { + type: 'codespan', + raw: cap[0], + text, + }; + } + } + br(src) { + const cap = this.rules.inline.br.exec(src); + if (cap) { + return { + type: 'br', + raw: cap[0], + }; + } + } + del(src) { + const cap = this.rules.inline.del.exec(src); + if (cap) { + return { + type: 'del', + raw: cap[0], + text: cap[2], + tokens: this.lexer.inlineTokens(cap[2]), + }; + } + } + autolink(src) { + const cap = this.rules.inline.autolink.exec(src); + if (cap) { + let text, href; + if (cap[2] === '@') { + text = cap[1]; + href = 'mailto:' + text; + } + else { + text = cap[1]; + href = text; + } + return { + type: 'link', + raw: cap[0], + text, + href, + tokens: [ + { + type: 'text', + raw: text, + text, + }, + ], + }; + } + } + url(src) { + let cap; + if (cap = this.rules.inline.url.exec(src)) { + let text, href; + if (cap[2] === '@') { + text = cap[0]; + href = 'mailto:' + text; + } + else { + // do extended autolink path validation + let prevCapZero; + do { + prevCapZero = cap[0]; + cap[0] = this.rules.inline._backpedal.exec(cap[0])?.[0] ?? ''; + } while (prevCapZero !== cap[0]); + text = cap[0]; + if (cap[1] === 'www.') { + href = 'http://' + cap[0]; + } + else { + href = cap[0]; + } + } + return { + type: 'link', + raw: cap[0], + text, + href, + tokens: [ + { + type: 'text', + raw: text, + text, + }, + ], + }; + } + } + inlineText(src) { + const cap = this.rules.inline.text.exec(src); + if (cap) { + const escaped = this.lexer.state.inRawBlock; + return { + type: 'text', + raw: cap[0], + text: cap[0], + escaped, + }; + } + } +} + +/** + * Block Lexer + */ +class _Lexer { + tokens; + options; + state; + tokenizer; + inlineQueue; + constructor(options) { + // TokenList cannot be created in one go + this.tokens = []; + this.tokens.links = Object.create(null); + this.options = options || _defaults; + this.options.tokenizer = this.options.tokenizer || new _Tokenizer(); + this.tokenizer = this.options.tokenizer; + this.tokenizer.options = this.options; + this.tokenizer.lexer = this; + this.inlineQueue = []; + this.state = { + inLink: false, + inRawBlock: false, + top: true, + }; + const rules = { + other, + block: block.normal, + inline: inline.normal, + }; + if (this.options.pedantic) { + rules.block = block.pedantic; + rules.inline = inline.pedantic; + } + else if (this.options.gfm) { + rules.block = block.gfm; + if (this.options.breaks) { + rules.inline = inline.breaks; + } + else { + rules.inline = inline.gfm; + } + } + this.tokenizer.rules = rules; + } + /** + * Expose Rules + */ + static get rules() { + return { + block, + inline, + }; + } + /** + * Static Lex Method + */ + static lex(src, options) { + const lexer = new _Lexer(options); + return lexer.lex(src); + } + /** + * Static Lex Inline Method + */ + static lexInline(src, options) { + const lexer = new _Lexer(options); + return lexer.inlineTokens(src); + } + /** + * Preprocessing + */ + lex(src) { + src = src.replace(other.carriageReturn, '\n'); + this.blockTokens(src, this.tokens); + for (let i = 0; i < this.inlineQueue.length; i++) { + const next = this.inlineQueue[i]; + this.inlineTokens(next.src, next.tokens); + } + this.inlineQueue = []; + return this.tokens; + } + blockTokens(src, tokens = [], lastParagraphClipped = false) { + if (this.options.pedantic) { + src = src.replace(other.tabCharGlobal, ' ').replace(other.spaceLine, ''); + } + while (src) { + let token; + if (this.options.extensions?.block?.some((extTokenizer) => { + if (token = extTokenizer.call({ lexer: this }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + return false; + })) { + continue; + } + // newline + if (token = this.tokenizer.space(src)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + if (token.raw.length === 1 && lastToken !== undefined) { + // if there's a single \n as a spacer, it's terminating the last line, + // so move it there so that we don't get unnecessary paragraph tags + lastToken.raw += '\n'; + } + else { + tokens.push(token); + } + continue; + } + // code + if (token = this.tokenizer.code(src)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + // An indented code block cannot interrupt a paragraph. + if (lastToken?.type === 'paragraph' || lastToken?.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.at(-1).src = lastToken.text; + } + else { + tokens.push(token); + } + continue; + } + // fences + if (token = this.tokenizer.fences(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // heading + if (token = this.tokenizer.heading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // hr + if (token = this.tokenizer.hr(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // blockquote + if (token = this.tokenizer.blockquote(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // list + if (token = this.tokenizer.list(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // html + if (token = this.tokenizer.html(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // def + if (token = this.tokenizer.def(src)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + if (lastToken?.type === 'paragraph' || lastToken?.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.raw; + this.inlineQueue.at(-1).src = lastToken.text; + } + else if (!this.tokens.links[token.tag]) { + this.tokens.links[token.tag] = { + href: token.href, + title: token.title, + }; + } + continue; + } + // table (gfm) + if (token = this.tokenizer.table(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // lheading + if (token = this.tokenizer.lheading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // top-level paragraph + // prevent paragraph consuming extensions by clipping 'src' to extension start + let cutSrc = src; + if (this.options.extensions?.startBlock) { + let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; + this.options.extensions.startBlock.forEach((getStartIndex) => { + tempStart = getStartIndex.call({ lexer: this }, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { + startIndex = Math.min(startIndex, tempStart); + } + }); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + } + if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) { + const lastToken = tokens.at(-1); + if (lastParagraphClipped && lastToken?.type === 'paragraph') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue.at(-1).src = lastToken.text; + } + else { + tokens.push(token); + } + lastParagraphClipped = cutSrc.length !== src.length; + src = src.substring(token.raw.length); + continue; + } + // text + if (token = this.tokenizer.text(src)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + if (lastToken?.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue.at(-1).src = lastToken.text; + } + else { + tokens.push(token); + } + continue; + } + if (src) { + const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + if (this.options.silent) { + console.error(errMsg); + break; + } + else { + throw new Error(errMsg); + } + } + } + this.state.top = true; + return tokens; + } + inline(src, tokens = []) { + this.inlineQueue.push({ src, tokens }); + return tokens; + } + /** + * Lexing/Compiling + */ + inlineTokens(src, tokens = []) { + // String with links masked to avoid interference with em and strong + let maskedSrc = src; + let match = null; + // Mask out reflinks + if (this.tokens.links) { + const links = Object.keys(this.tokens.links); + if (links.length > 0) { + while ((match = this.tokenizer.rules.inline.reflinkSearch.exec(maskedSrc)) != null) { + if (links.includes(match[0].slice(match[0].lastIndexOf('[') + 1, -1))) { + maskedSrc = maskedSrc.slice(0, match.index) + + '[' + 'a'.repeat(match[0].length - 2) + ']' + + maskedSrc.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex); + } + } + } + } + // Mask out other blocks + while ((match = this.tokenizer.rules.inline.blockSkip.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex); + } + // Mask out escaped characters + while ((match = this.tokenizer.rules.inline.anyPunctuation.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '++' + maskedSrc.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex); + } + let keepPrevChar = false; + let prevChar = ''; + while (src) { + if (!keepPrevChar) { + prevChar = ''; + } + keepPrevChar = false; + let token; + // extensions + if (this.options.extensions?.inline?.some((extTokenizer) => { + if (token = extTokenizer.call({ lexer: this }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + return false; + })) { + continue; + } + // escape + if (token = this.tokenizer.escape(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // tag + if (token = this.tokenizer.tag(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // link + if (token = this.tokenizer.link(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // reflink, nolink + if (token = this.tokenizer.reflink(src, this.tokens.links)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + if (token.type === 'text' && lastToken?.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + // em & strong + if (token = this.tokenizer.emStrong(src, maskedSrc, prevChar)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // code + if (token = this.tokenizer.codespan(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // br + if (token = this.tokenizer.br(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // del (gfm) + if (token = this.tokenizer.del(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // autolink + if (token = this.tokenizer.autolink(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // url (gfm) + if (!this.state.inLink && (token = this.tokenizer.url(src))) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // text + // prevent inlineText consuming extensions by clipping 'src' to extension start + let cutSrc = src; + if (this.options.extensions?.startInline) { + let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; + this.options.extensions.startInline.forEach((getStartIndex) => { + tempStart = getStartIndex.call({ lexer: this }, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { + startIndex = Math.min(startIndex, tempStart); + } + }); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + } + if (token = this.tokenizer.inlineText(cutSrc)) { + src = src.substring(token.raw.length); + if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started + prevChar = token.raw.slice(-1); + } + keepPrevChar = true; + const lastToken = tokens.at(-1); + if (lastToken?.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + if (src) { + const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + if (this.options.silent) { + console.error(errMsg); + break; + } + else { + throw new Error(errMsg); + } + } + } + return tokens; + } +} + +/** + * Renderer + */ +class _Renderer { + options; + parser; // set by the parser + constructor(options) { + this.options = options || _defaults; + } + space(token) { + return ''; + } + code({ text, lang, escaped }) { + const langString = (lang || '').match(other.notSpaceStart)?.[0]; + const code = text.replace(other.endingNewline, '') + '\n'; + if (!langString) { + return '
'
+                + (escaped ? code : escape(code, true))
+                + '
\n'; + } + return '
'
+            + (escaped ? code : escape(code, true))
+            + '
\n'; + } + blockquote({ tokens }) { + const body = this.parser.parse(tokens); + return `
\n${body}
\n`; + } + html({ text }) { + return text; + } + heading({ tokens, depth }) { + return `${this.parser.parseInline(tokens)}\n`; + } + hr(token) { + return '
\n'; + } + list(token) { + const ordered = token.ordered; + const start = token.start; + let body = ''; + for (let j = 0; j < token.items.length; j++) { + const item = token.items[j]; + body += this.listitem(item); + } + const type = ordered ? 'ol' : 'ul'; + const startAttr = (ordered && start !== 1) ? (' start="' + start + '"') : ''; + return '<' + type + startAttr + '>\n' + body + '\n'; + } + listitem(item) { + let itemBody = ''; + if (item.task) { + const checkbox = this.checkbox({ checked: !!item.checked }); + if (item.loose) { + if (item.tokens[0]?.type === 'paragraph') { + item.tokens[0].text = checkbox + ' ' + item.tokens[0].text; + if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') { + item.tokens[0].tokens[0].text = checkbox + ' ' + escape(item.tokens[0].tokens[0].text); + item.tokens[0].tokens[0].escaped = true; + } + } + else { + item.tokens.unshift({ + type: 'text', + raw: checkbox + ' ', + text: checkbox + ' ', + escaped: true, + }); + } + } + else { + itemBody += checkbox + ' '; + } + } + itemBody += this.parser.parse(item.tokens, !!item.loose); + return `
  • ${itemBody}
  • \n`; + } + checkbox({ checked }) { + return ''; + } + paragraph({ tokens }) { + return `

    ${this.parser.parseInline(tokens)}

    \n`; + } + table(token) { + let header = ''; + // header + let cell = ''; + for (let j = 0; j < token.header.length; j++) { + cell += this.tablecell(token.header[j]); + } + header += this.tablerow({ text: cell }); + let body = ''; + for (let j = 0; j < token.rows.length; j++) { + const row = token.rows[j]; + cell = ''; + for (let k = 0; k < row.length; k++) { + cell += this.tablecell(row[k]); + } + body += this.tablerow({ text: cell }); + } + if (body) + body = `${body}`; + return '\n' + + '\n' + + header + + '\n' + + body + + '
    \n'; + } + tablerow({ text }) { + return `\n${text}\n`; + } + tablecell(token) { + const content = this.parser.parseInline(token.tokens); + const type = token.header ? 'th' : 'td'; + const tag = token.align + ? `<${type} align="${token.align}">` + : `<${type}>`; + return tag + content + `\n`; + } + /** + * span level renderer + */ + strong({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + em({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + codespan({ text }) { + return `${escape(text, true)}`; + } + br(token) { + return '
    '; + } + del({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + link({ href, title, tokens }) { + const text = this.parser.parseInline(tokens); + const cleanHref = cleanUrl(href); + if (cleanHref === null) { + return text; + } + href = cleanHref; + let out = '
    '; + return out; + } + image({ href, title, text }) { + const cleanHref = cleanUrl(href); + if (cleanHref === null) { + return escape(text); + } + href = cleanHref; + let out = `${text} { + const tokens = genericToken[childTokens].flat(Infinity); + values = values.concat(this.walkTokens(tokens, callback)); + }); + } + else if (genericToken.tokens) { + values = values.concat(this.walkTokens(genericToken.tokens, callback)); + } + } + } + } + return values; + } + use(...args) { + const extensions = this.defaults.extensions || { renderers: {}, childTokens: {} }; + args.forEach((pack) => { + // copy options to new object + const opts = { ...pack }; + // set async to true if it was set to true before + opts.async = this.defaults.async || opts.async || false; + // ==-- Parse "addon" extensions --== // + if (pack.extensions) { + pack.extensions.forEach((ext) => { + if (!ext.name) { + throw new Error('extension name required'); + } + if ('renderer' in ext) { // Renderer extensions + const prevRenderer = extensions.renderers[ext.name]; + if (prevRenderer) { + // Replace extension with func to run new extension but fall back if false + extensions.renderers[ext.name] = function (...args) { + let ret = ext.renderer.apply(this, args); + if (ret === false) { + ret = prevRenderer.apply(this, args); + } + return ret; + }; + } + else { + extensions.renderers[ext.name] = ext.renderer; + } + } + if ('tokenizer' in ext) { // Tokenizer Extensions + if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) { + throw new Error("extension level must be 'block' or 'inline'"); + } + const extLevel = extensions[ext.level]; + if (extLevel) { + extLevel.unshift(ext.tokenizer); + } + else { + extensions[ext.level] = [ext.tokenizer]; + } + if (ext.start) { // Function to check for start of token + if (ext.level === 'block') { + if (extensions.startBlock) { + extensions.startBlock.push(ext.start); + } + else { + extensions.startBlock = [ext.start]; + } + } + else if (ext.level === 'inline') { + if (extensions.startInline) { + extensions.startInline.push(ext.start); + } + else { + extensions.startInline = [ext.start]; + } + } + } + } + if ('childTokens' in ext && ext.childTokens) { // Child tokens to be visited by walkTokens + extensions.childTokens[ext.name] = ext.childTokens; + } + }); + opts.extensions = extensions; + } + // ==-- Parse "overwrite" extensions --== // + if (pack.renderer) { + const renderer = this.defaults.renderer || new _Renderer(this.defaults); + for (const prop in pack.renderer) { + if (!(prop in renderer)) { + throw new Error(`renderer '${prop}' does not exist`); + } + if (['options', 'parser'].includes(prop)) { + // ignore options property + continue; + } + const rendererProp = prop; + const rendererFunc = pack.renderer[rendererProp]; + const prevRenderer = renderer[rendererProp]; + // Replace renderer with func to run extension, but fall back if false + renderer[rendererProp] = (...args) => { + let ret = rendererFunc.apply(renderer, args); + if (ret === false) { + ret = prevRenderer.apply(renderer, args); + } + return ret || ''; + }; + } + opts.renderer = renderer; + } + if (pack.tokenizer) { + const tokenizer = this.defaults.tokenizer || new _Tokenizer(this.defaults); + for (const prop in pack.tokenizer) { + if (!(prop in tokenizer)) { + throw new Error(`tokenizer '${prop}' does not exist`); + } + if (['options', 'rules', 'lexer'].includes(prop)) { + // ignore options, rules, and lexer properties + continue; + } + const tokenizerProp = prop; + const tokenizerFunc = pack.tokenizer[tokenizerProp]; + const prevTokenizer = tokenizer[tokenizerProp]; + // Replace tokenizer with func to run extension, but fall back if false + // @ts-expect-error cannot type tokenizer function dynamically + tokenizer[tokenizerProp] = (...args) => { + let ret = tokenizerFunc.apply(tokenizer, args); + if (ret === false) { + ret = prevTokenizer.apply(tokenizer, args); + } + return ret; + }; + } + opts.tokenizer = tokenizer; + } + // ==-- Parse Hooks extensions --== // + if (pack.hooks) { + const hooks = this.defaults.hooks || new _Hooks(); + for (const prop in pack.hooks) { + if (!(prop in hooks)) { + throw new Error(`hook '${prop}' does not exist`); + } + if (['options', 'block'].includes(prop)) { + // ignore options and block properties + continue; + } + const hooksProp = prop; + const hooksFunc = pack.hooks[hooksProp]; + const prevHook = hooks[hooksProp]; + if (_Hooks.passThroughHooks.has(prop)) { + // @ts-expect-error cannot type hook function dynamically + hooks[hooksProp] = (arg) => { + if (this.defaults.async) { + return Promise.resolve(hooksFunc.call(hooks, arg)).then(ret => { + return prevHook.call(hooks, ret); + }); + } + const ret = hooksFunc.call(hooks, arg); + return prevHook.call(hooks, ret); + }; + } + else { + // @ts-expect-error cannot type hook function dynamically + hooks[hooksProp] = (...args) => { + let ret = hooksFunc.apply(hooks, args); + if (ret === false) { + ret = prevHook.apply(hooks, args); + } + return ret; + }; + } + } + opts.hooks = hooks; + } + // ==-- Parse WalkTokens extensions --== // + if (pack.walkTokens) { + const walkTokens = this.defaults.walkTokens; + const packWalktokens = pack.walkTokens; + opts.walkTokens = function (token) { + let values = []; + values.push(packWalktokens.call(this, token)); + if (walkTokens) { + values = values.concat(walkTokens.call(this, token)); + } + return values; + }; + } + this.defaults = { ...this.defaults, ...opts }; + }); + return this; + } + setOptions(opt) { + this.defaults = { ...this.defaults, ...opt }; + return this; + } + lexer(src, options) { + return _Lexer.lex(src, options ?? this.defaults); + } + parser(tokens, options) { + return _Parser.parse(tokens, options ?? this.defaults); + } + parseMarkdown(blockType) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const parse = (src, options) => { + const origOpt = { ...options }; + const opt = { ...this.defaults, ...origOpt }; + const throwError = this.onError(!!opt.silent, !!opt.async); + // throw error if an extension set async to true but parse was called with async: false + if (this.defaults.async === true && origOpt.async === false) { + return throwError(new Error('marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise.')); + } + // throw error in case of non string input + if (typeof src === 'undefined' || src === null) { + return throwError(new Error('marked(): input parameter is undefined or null')); + } + if (typeof src !== 'string') { + return throwError(new Error('marked(): input parameter is of type ' + + Object.prototype.toString.call(src) + ', string expected')); + } + if (opt.hooks) { + opt.hooks.options = opt; + opt.hooks.block = blockType; + } + const lexer = opt.hooks ? opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline); + const parser = opt.hooks ? opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline); + if (opt.async) { + return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) + .then(src => lexer(src, opt)) + .then(tokens => opt.hooks ? opt.hooks.processAllTokens(tokens) : tokens) + .then(tokens => opt.walkTokens ? Promise.all(this.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens) + .then(tokens => parser(tokens, opt)) + .then(html => opt.hooks ? opt.hooks.postprocess(html) : html) + .catch(throwError); + } + try { + if (opt.hooks) { + src = opt.hooks.preprocess(src); + } + let tokens = lexer(src, opt); + if (opt.hooks) { + tokens = opt.hooks.processAllTokens(tokens); + } + if (opt.walkTokens) { + this.walkTokens(tokens, opt.walkTokens); + } + let html = parser(tokens, opt); + if (opt.hooks) { + html = opt.hooks.postprocess(html); + } + return html; + } + catch (e) { + return throwError(e); + } + }; + return parse; + } + onError(silent, async) { + return (e) => { + e.message += '\nPlease report this to https://github.com/markedjs/marked.'; + if (silent) { + const msg = '

    An error occurred:

    '
    +                    + escape(e.message + '', true)
    +                    + '
    '; + if (async) { + return Promise.resolve(msg); + } + return msg; + } + if (async) { + return Promise.reject(e); + } + throw e; + }; + } +} + +const markedInstance = new Marked(); +function marked(src, opt) { + return markedInstance.parse(src, opt); +} +/** + * Sets the default options. + * + * @param options Hash of options + */ +marked.options = + marked.setOptions = function (options) { + markedInstance.setOptions(options); + marked.defaults = markedInstance.defaults; + changeDefaults(marked.defaults); + return marked; + }; +/** + * Gets the original marked default options. + */ +marked.getDefaults = _getDefaults; +marked.defaults = _defaults; +/** + * Use Extension + */ +marked.use = function (...args) { + markedInstance.use(...args); + marked.defaults = markedInstance.defaults; + changeDefaults(marked.defaults); + return marked; +}; +/** + * Run callback for every token + */ +marked.walkTokens = function (tokens, callback) { + return markedInstance.walkTokens(tokens, callback); +}; +/** + * Compiles markdown to HTML without enclosing `p` tag. + * + * @param src String of markdown source to be compiled + * @param options Hash of options + * @return String of compiled HTML + */ +marked.parseInline = markedInstance.parseInline; +/** + * Expose + */ +marked.Parser = _Parser; +marked.parser = _Parser.parse; +marked.Renderer = _Renderer; +marked.TextRenderer = _TextRenderer; +marked.Lexer = _Lexer; +marked.lexer = _Lexer.lex; +marked.Tokenizer = _Tokenizer; +marked.Hooks = _Hooks; +marked.parse = marked; +const options = marked.options; +const setOptions = marked.setOptions; +const use = marked.use; +const walkTokens = marked.walkTokens; +const parseInline = marked.parseInline; +const parse = marked; +const parser = _Parser.parse; +const lexer = _Lexer.lex; + +export { _Hooks as Hooks, _Lexer as Lexer, Marked, _Parser as Parser, _Renderer as Renderer, _TextRenderer as TextRenderer, _Tokenizer as Tokenizer, _defaults as defaults, _getDefaults as getDefaults, lexer, marked, options, parse, parseInline, parser, setOptions, use, walkTokens }; +//# sourceMappingURL=marked.esm.js.map diff --git a/chrome/browser/resources/side_panel/reading_list/reading_list_app.ts b/chrome/browser/resources/side_panel/reading_list/reading_list_app.ts index 87debbe4418a69..9b26ac5a4898a5 100644 --- a/chrome/browser/resources/side_panel/reading_list/reading_list_app.ts +++ b/chrome/browser/resources/side_panel/reading_list/reading_list_app.ts @@ -59,9 +59,9 @@ export class ReadingListAppElement extends ReadingListAppElementBase { static override get properties() { return { - unreadItems_: {type: Array}, - readItems_: {type: Array}, - currentPageActionButtonState_: {type: Number}, + unreadItems_: {type: Array}, + readItems_: {type: Array}, + currentPageActionButtonState_: {type: Number}, buttonRipples: {type: Boolean}, loadingContent_: {type: Boolean}, }; diff --git a/chrome/browser/resources/webui_gallery/demos/cr_icons/cr_icons_demo.html.ts b/chrome/browser/resources/webui_gallery/demos/cr_icons/cr_icons_demo.html.ts index ed7d70b7ce3950..584f6dd6af8dfd 100644 --- a/chrome/browser/resources/webui_gallery/demos/cr_icons/cr_icons_demo.html.ts +++ b/chrome/browser/resources/webui_gallery/demos/cr_icons/cr_icons_demo.html.ts @@ -31,7 +31,9 @@ export function getHtml(this: CrIconsDemoElement) { - + + + diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn index c1c10d6d72313a..f02565dfacb8e0 100644 --- a/chrome/browser/ui/BUILD.gn +++ b/chrome/browser/ui/BUILD.gn @@ -1565,6 +1565,16 @@ static_library("ui") { "webui/side_panel/bookmarks/bookmarks_page_handler.h", "webui/side_panel/bookmarks/bookmarks_side_panel_ui.cc", "webui/side_panel/bookmarks/bookmarks_side_panel_ui.h", + "webui/side_panel/chat/api/api_request_helper.cc", + "webui/side_panel/chat/api/api_request_helper.h", + "webui/side_panel/chat/api/completion_api_client.cc", + "webui/side_panel/chat/api/completion_api_client.h", + "webui/side_panel/chat/chat_page_handler.cc", + "webui/side_panel/chat/chat_page_handler.h", + "webui/side_panel/chat/page_content_extractor_helper.cc", + "webui/side_panel/chat/page_content_extractor_helper.h", + "webui/side_panel/chat/chat_ui.cc", + "webui/side_panel/chat/chat_ui.h", "webui/side_panel/companion/companion_page_handler.cc", "webui/side_panel/companion/companion_page_handler.h", "webui/side_panel/companion/companion_side_panel_untrusted_ui.cc", @@ -1746,6 +1756,8 @@ static_library("ui") { "//chrome/browser/ui/webui/searchbox", "//chrome/browser/ui/webui/settings", "//chrome/browser/ui/webui/settings:impl", + "//chrome/common/chat:mojo_bindings", + "//chrome/browser/ui/webui/side_panel/chat:mojo_bindings", "//chrome/browser/ui/webui/signin", "//chrome/browser/ui/webui/signin:login", "//chrome/browser/ui/webui/signin:login_impl", diff --git a/chrome/browser/ui/actions/chrome_action_id.h b/chrome/browser/ui/actions/chrome_action_id.h index 3c8c3fd5b4d2c4..5477ec4f5067c1 100644 --- a/chrome/browser/ui/actions/chrome_action_id.h +++ b/chrome/browser/ui/actions/chrome_action_id.h @@ -550,6 +550,7 @@ E(kActionSidePanelShowLensOverlayResults, IDC_CONTENT_CONTEXT_LENS_OVERLAY) \ E(kActionSidePanelShowReadAnything) \ E(kActionSidePanelShowReadingList, IDC_READING_LIST_MENU_SHOW_UI) \ + E(kActionAIChat, IDC_SHOW_AI_CHAT) \ E(kActionSidePanelShowSearchCompanion, IDC_SHOW_SEARCH_COMPANION) \ E(kActionSidePanelShowShoppingInsights) \ E(kActionSidePanelShowSideSearch) \ diff --git a/chrome/browser/ui/browser_actions.cc b/chrome/browser/ui/browser_actions.cc index a354a5b785589b..e46ad6f1f891a3 100644 --- a/chrome/browser/ui/browser_actions.cc +++ b/chrome/browser/ui/browser_actions.cc @@ -143,6 +143,9 @@ void BrowserActions::InitializeBrowserActions() { IDS_READ_LATER_TITLE, IDS_READ_LATER_TITLE, kReadingListIcon, kActionSidePanelShowReadingList, browser, true), + SidePanelAction(SidePanelEntryId::kAIChat, IDS_AI_CHAT_TITLE, + IDS_AI_CHAT_TITLE, kChatIcon, kActionAIChat, + browser, false), SidePanelAction(SidePanelEntryId::kAboutThisSite, IDS_PAGE_INFO_ABOUT_THIS_PAGE_TITLE, IDS_PAGE_INFO_ABOUT_THIS_PAGE_TITLE, diff --git a/chrome/browser/ui/browser_element_identifiers.cc b/chrome/browser/ui/browser_element_identifiers.cc index ddc76506180884..1e58a5439a988a 100644 --- a/chrome/browser/ui/browser_element_identifiers.cc +++ b/chrome/browser/ui/browser_element_identifiers.cc @@ -9,6 +9,7 @@ // Please keep this list alphabetized. DEFINE_ELEMENT_IDENTIFIER_VALUE(kAddCurrentTabToReadingListElementId); +DECLARE_ELEMENT_IDENTIFIER_VALUE(kAIChatSidePanelElementId); DEFINE_ELEMENT_IDENTIFIER_VALUE( kAnonymizedUrlCollectionPersonalizationSettingId); DEFINE_ELEMENT_IDENTIFIER_VALUE(kAppUninstallDialogOkButtonId); @@ -26,6 +27,7 @@ DEFINE_ELEMENT_IDENTIFIER_VALUE(kBookmarkBarElementId); DEFINE_ELEMENT_IDENTIFIER_VALUE(kBookmarkSidePanelWebViewElementId); DEFINE_ELEMENT_IDENTIFIER_VALUE(kBookmarkStarViewElementId); DEFINE_ELEMENT_IDENTIFIER_VALUE(kBrowserViewElementId); +DEFINE_ELEMENT_IDENTIFIER_VALUE(kChatSidePanelWebViewElementId); DEFINE_ELEMENT_IDENTIFIER_VALUE(kConstrainedDialogWebViewElementId); DEFINE_ELEMENT_IDENTIFIER_VALUE(kCookieControlsIconElementId); DEFINE_ELEMENT_IDENTIFIER_VALUE(kCustomizeChromeSidePanelWebViewElementId); diff --git a/chrome/browser/ui/browser_element_identifiers.h b/chrome/browser/ui/browser_element_identifiers.h index 536cba8cfb5bd9..c1455c2eb9c2fa 100644 --- a/chrome/browser/ui/browser_element_identifiers.h +++ b/chrome/browser/ui/browser_element_identifiers.h @@ -35,6 +35,7 @@ DECLARE_ELEMENT_IDENTIFIER_VALUE(kBookmarkBarElementId); DECLARE_ELEMENT_IDENTIFIER_VALUE(kBookmarkSidePanelWebViewElementId); DECLARE_ELEMENT_IDENTIFIER_VALUE(kBookmarkStarViewElementId); DECLARE_ELEMENT_IDENTIFIER_VALUE(kBrowserViewElementId); +DECLARE_ELEMENT_IDENTIFIER_VALUE(kChatSidePanelWebViewElementId); DECLARE_ELEMENT_IDENTIFIER_VALUE(kConstrainedDialogWebViewElementId); DECLARE_ELEMENT_IDENTIFIER_VALUE(kCookieControlsIconElementId); DECLARE_ELEMENT_IDENTIFIER_VALUE(kCustomizeChromeSidePanelWebViewElementId); diff --git a/chrome/browser/ui/color/chrome_color_id.h b/chrome/browser/ui/color/chrome_color_id.h index a4d56736d3d329..3aba807f2d1f9b 100644 --- a/chrome/browser/ui/color/chrome_color_id.h +++ b/chrome/browser/ui/color/chrome_color_id.h @@ -858,6 +858,17 @@ /* Window control button background colors. */ \ E_CPONLY(kColorWindowControlButtonBackgroundActive) \ E_CPONLY(kColorWindowControlButtonBackgroundInactive) \ + /* Yep Chat Colors. */ \ + E_CPONLY(kColorYepChatPanelBackground) \ + E_CPONLY(kColorYepChatPanelBorder) \ + E_CPONLY(kColorYepChatPromptBackground) \ + E_CPONLY(kColorYepChatSiteInfoVerticalBarBackground) \ + E_CPONLY(kColorYepChatPrimaryText) \ + E_CPONLY(kColorYepChatSecondaryText) \ + E_CPONLY(kColorYepChatTertiaryText) \ + E_CPONLY(kColorYepChatSendButtonBackground) \ + E_CPONLY(kColorYepChatSendButtonForeground) \ + E_CPONLY(kColorYepChatError) \ #if BUILDFLAG(IS_CHROMEOS) #define CHROME_PLATFORM_SPECIFIC_COLOR_IDS \ diff --git a/chrome/browser/ui/color/chrome_color_mixer.cc b/chrome/browser/ui/color/chrome_color_mixer.cc index 6a0ffbd53aeab6..f91fb7ed1208a3 100644 --- a/chrome/browser/ui/color/chrome_color_mixer.cc +++ b/chrome/browser/ui/color/chrome_color_mixer.cc @@ -884,6 +884,33 @@ void AddChromeColorMixer(ui::ColorProvider* provider, gfx::kGoogleBlue500, kColorReadAnythingBackgroundYellow, color_utils::kMinimumVisibleContrastRatio); + /* Yep Chat Colors */ + mixer[kColorYepChatPromptBackground] = { + dark_mode ? SkColorSetRGB(24, 24, 28) : SkColorSetRGB(235, 233, 240)}; + mixer[kColorYepChatPanelBackground] = {dark_mode ? SkColorSetRGB(42, 41, 51) + : SK_ColorWHITE}; + mixer[kColorYepChatSiteInfoVerticalBarBackground] = { + dark_mode ? SkColorSetARGB(26, 255, 255, 255) + : SkColorSetARGB(26, 0, 0, 0)}; + mixer[kColorYepChatPrimaryText] = {dark_mode + ? SkColorSetARGB(217, 255, 255, 255) + : SkColorSetARGB(217, 0, 0, 0)}; + mixer[kColorYepChatSecondaryText] = {dark_mode + ? SkColorSetARGB(140, 255, 255, 255) + : SkColorSetARGB(140, 0, 0, 0)}; + mixer[kColorYepChatTertiaryText] = {dark_mode + ? SkColorSetARGB(64, 255, 255, 255) + : SkColorSetARGB(64, 0, 0, 0)}; + mixer[kColorYepChatPanelBorder] = {dark_mode + ? SkColorSetARGB(26, 255, 255, 255) + : SkColorSetARGB(26, 0, 0, 0)}; + mixer[kColorYepChatSendButtonBackground] = {dark_mode ? SK_ColorWHITE + : SK_ColorBLACK}; + mixer[kColorYepChatSendButtonForeground] = {dark_mode ? SK_ColorBLACK + : SK_ColorWHITE}; + + mixer[kColorYepChatError] = {SK_ColorRED}; + // Apply high contrast recipes if necessary. if (!ShouldApplyHighContrastColors(key)) { return; diff --git a/chrome/browser/ui/startup/infobar_utils.cc b/chrome/browser/ui/startup/infobar_utils.cc index 371869078eab56..4755520300339d 100644 --- a/chrome/browser/ui/startup/infobar_utils.cc +++ b/chrome/browser/ui/startup/infobar_utils.cc @@ -154,9 +154,11 @@ void AddInfoBarsIfNecessary(Browser* browser, infobars::ContentInfoBarManager* infobar_manager = infobars::ContentInfoBarManager::FromWebContents(web_contents); + /* hide google api key request popup if (!google_apis::HasAPIKeyConfigured()) { GoogleApiKeysInfoBarDelegate::Create(infobar_manager); } + */ if (ObsoleteSystem::IsObsoleteNowOrSoon()) { PrefService* local_state = g_browser_process->local_state(); diff --git a/chrome/browser/ui/views/frame/browser_view.cc b/chrome/browser/ui/views/frame/browser_view.cc index 8c08f4c3f10faa..3e630b0bdc3a98 100644 --- a/chrome/browser/ui/views/frame/browser_view.cc +++ b/chrome/browser/ui/views/frame/browser_view.cc @@ -1029,11 +1029,8 @@ BrowserView::BrowserView(std::unique_ptr browser) right_aligned_side_panel_separator_ = AddChildView(std::make_unique()); - const bool is_right_aligned = GetProfile()->GetPrefs()->GetBoolean( - prefs::kSidePanelHorizontalAlignment); unified_side_panel_ = AddChildView(std::make_unique( - this, is_right_aligned ? SidePanel::HorizontalAlignment::kRight - : SidePanel::HorizontalAlignment::kLeft)); + this, SidePanel::HorizontalAlignment::kRight)); left_aligned_side_panel_separator_ = AddChildView(std::make_unique()); side_panel_rounded_corner_ = diff --git a/chrome/browser/ui/views/side_panel/BUILD.gn b/chrome/browser/ui/views/side_panel/BUILD.gn index f737fdd6743633..5add7206b112fc 100644 --- a/chrome/browser/ui/views/side_panel/BUILD.gn +++ b/chrome/browser/ui/views/side_panel/BUILD.gn @@ -15,6 +15,10 @@ source_set("side_panel") { sources = [ "bookmarks/bookmarks_side_panel_coordinator.cc", "bookmarks/bookmarks_side_panel_coordinator.h", + "chat/chat_side_panel_coordinator.cc", + "chat/chat_side_panel_coordinator.h", + "chat/chat_side_panel_web_view.cc", + "chat/chat_side_panel_web_view.h", "companion/companion_side_panel_controller_utils.h", "companion/companion_tab_helper.cc", "companion/companion_tab_helper.h", @@ -176,6 +180,8 @@ source_set("side_panel") { "//chrome/browser/ui/user_education", "//chrome/browser/ui/views/toolbar", "//chrome/browser/ui/webui/top_chrome", + "//chrome/common/chat:mojo_bindings", + "//chrome/browser/ui/webui/side_panel/chat:mojo_bindings", "//chrome/common", "//chrome/common:constants", "//components/accessibility:reading", diff --git a/chrome/browser/ui/views/side_panel/chat/chat_side_panel_coordinator.cc b/chrome/browser/ui/views/side_panel/chat/chat_side_panel_coordinator.cc new file mode 100644 index 00000000000000..6568abf1483d79 --- /dev/null +++ b/chrome/browser/ui/views/side_panel/chat/chat_side_panel_coordinator.cc @@ -0,0 +1,59 @@ +#include "chat_side_panel_coordinator.h" + +#include + +#include "base/functional/callback.h" +#include "chrome/app/vector_icons/vector_icons.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/views/frame/browser_view.h" +#include "chrome/browser/ui/views/side_panel/chat/chat_side_panel_web_view.h" +#include "chrome/browser/ui/views/side_panel/side_panel_entry.h" +#include "chrome/browser/ui/views/side_panel/side_panel_registry.h" +#include "chrome/browser/ui/views/toolbar/toolbar_view.h" +#include "chrome/grit/generated_resources.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/base/models/image_model.h" +#include "ui/base/ui_base_features.h" +#include "ui/views/vector_icons.h" + +ChatSidePanelCoordinator::ChatSidePanelCoordinator(Browser* browser) + : BrowserUserData(*browser) {} + +ChatSidePanelCoordinator::~ChatSidePanelCoordinator() = default; + +void ChatSidePanelCoordinator::CreateAndRegisterEntry( + SidePanelRegistry* global_registry) { + global_registry->Register(std::make_unique( + SidePanelEntry::Id::kAIChat, + base::BindRepeating(&ChatSidePanelCoordinator::CreateChatWebView, + base::Unretained(this)))); +} + +std::unique_ptr ChatSidePanelCoordinator::CreateChatWebView() { + return std::make_unique(&GetBrowser(), + base::RepeatingClosure()); +} + +void ChatSidePanelCoordinator::UpdateOpeningPanelId(SidePanelEntryId panel_id) { + auto* browser = &GetBrowser(); + auto* browser_view = BrowserView::GetBrowserViewForBrowser(browser); + if (browser_view) { + auto* toolbar = browser_view->toolbar(); + if (panel_id != SidePanelEntry::Id::kAIChat) { + toolbar->ResetHighlightForAIChatButton(); + } else { + toolbar->AddHighlightForAIChatButton(); + } + } +} + +void ChatSidePanelCoordinator::UpdateClosingPanelId(SidePanelEntryId panel_id) { + auto* browser = &GetBrowser(); + auto* browser_view = BrowserView::GetBrowserViewForBrowser(browser); + if (browser_view && panel_id == SidePanelEntry::Id::kAIChat) { + auto* toolbar = browser_view->toolbar(); + toolbar->ResetHighlightForAIChatButton(); + } +} + +BROWSER_USER_DATA_KEY_IMPL(ChatSidePanelCoordinator); diff --git a/chrome/browser/ui/views/side_panel/chat/chat_side_panel_coordinator.h b/chrome/browser/ui/views/side_panel/chat/chat_side_panel_coordinator.h new file mode 100644 index 00000000000000..a8bb080af7c68b --- /dev/null +++ b/chrome/browser/ui/views/side_panel/chat/chat_side_panel_coordinator.h @@ -0,0 +1,31 @@ +#ifndef CHROMIUM_CHAT_SIDE_PANEL_COORDINATOR_H +#define CHROMIUM_CHAT_SIDE_PANEL_COORDINATOR_H + +#include "chrome/browser/ui/browser_user_data.h" +#include "chrome/browser/ui/views/side_panel/side_panel_entry_id.h" + +class Browser; +class SidePanelRegistry; + +namespace views { +class View; +} // namespace views + +class ChatSidePanelCoordinator + : public BrowserUserData { + public: + explicit ChatSidePanelCoordinator(Browser* browser); + ~ChatSidePanelCoordinator() override; + + void CreateAndRegisterEntry(SidePanelRegistry* global_registry); + void UpdateOpeningPanelId(SidePanelEntryId panel_id); + void UpdateClosingPanelId(SidePanelEntryId panel_id); + + private: + friend class BrowserUserData; + std::unique_ptr CreateChatWebView(); + + BROWSER_USER_DATA_KEY_DECL(); +}; + +#endif // CHROMIUM_CHAT_SIDE_PANEL_COORDINATOR_H diff --git a/chrome/browser/ui/views/side_panel/chat/chat_side_panel_web_view.cc b/chrome/browser/ui/views/side_panel/chat/chat_side_panel_web_view.cc new file mode 100644 index 00000000000000..72b0e2f98aaa09 --- /dev/null +++ b/chrome/browser/ui/views/side_panel/chat/chat_side_panel_web_view.cc @@ -0,0 +1,103 @@ +#include "chat_side_panel_web_view.h" + +#include +#include + +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/bookmarks/bookmark_utils.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_element_identifiers.h" +#include "chrome/browser/ui/ui_features.h" +#include "chrome/browser/ui/views/frame/browser_view.h" +#include "chrome/common/webui_url_constants.h" +#include "chrome/grit/generated_resources.h" +#include "ui/base/metadata/metadata_impl_macros.h" +#include "ui/views/controls/label.h" +#include "ui/views/controls/webview/webview.h" +#include "ui/views/layout/fill_layout.h" +#include "ui/views/layout/flex_layout.h" +#include "ui/views/layout/layout_types.h" +#include "ui/views/view.h" +#include "ui/views/view_class_properties.h" +#include "url/gurl.h" +#include "services/service_manager/public/cpp/interface_provider.h" +#include "base/containers/contains.h" +#include "base/containers/fixed_flat_set.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" + +using SidePanelWebUIViewT_ChatUI = SidePanelWebUIViewT; +BEGIN_TEMPLATE_METADATA(SidePanelWebUIViewT_ChatUI, SidePanelWebUIViewT) +END_METADATA + +ChatSidePanelWebView::ChatSidePanelWebView(Browser* browser, + base::RepeatingClosure close_cb) + : SidePanelWebUIViewT( + base::BindRepeating(&ChatSidePanelWebView::UpdateActiveWebContents, + base::Unretained(this)), + close_cb, + std::make_unique>( + GURL(chrome::kChromeUIChatURL), + browser->profile(), + IDS_AI_CHAT_TITLE, + /*esc_closes_ui=*/false)), + browser_(browser), + weak_ptr_factory_(this) { + SetProperty(views::kElementIdentifierKey, kChatSidePanelWebViewElementId); + browser_->tab_strip_model()->AddObserver(this); +} + +void ChatSidePanelWebView::OnTabStripModelChanged( + TabStripModel* tab_strip_model, + const TabStripModelChange& change, + const TabStripSelectionChange& selection) { + if (GetVisible() && selection.active_tab_changed()) { + UpdateActiveSiteInfo(tab_strip_model->GetActiveWebContents()); + } +} + +void ChatSidePanelWebView::TabChangedAt(content::WebContents* contents, + int index, + TabChangeType change_type) { + if (GetVisible() && index == browser_->tab_strip_model()->active_index() && + change_type == TabChangeType::kAll) { + UpdateActiveSiteInfo(browser_->tab_strip_model()->GetWebContentsAt(index)); + } +} + +void ChatSidePanelWebView::UpdateActiveSiteInfo( + content::WebContents* contents) { + auto* controller = contents_wrapper()->GetWebUIController(); + if (!controller || !contents) { + return; + } + + chat::mojom::SiteInfoPtr site_info = chat::mojom::SiteInfo::New(); + site_info->title = base::UTF16ToUTF8(contents->GetTitle()); + + const GURL gurl = contents->GetLastCommittedURL(); + if (gurl.SchemeIsHTTPOrHTTPS()) { + site_info->url = gurl.spec(); + site_info->is_content_usable_in_conversations = true; + } else { + site_info->url = ""; + site_info->is_content_usable_in_conversations = false; + } + + controller->GetAs()->SetSiteInfo(site_info.Clone(), contents); +} + +base::WeakPtr ChatSidePanelWebView::GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); +} + +void ChatSidePanelWebView::UpdateActiveWebContents() { + UpdateActiveSiteInfo(browser_->tab_strip_model()->GetActiveWebContents()); +} + +ChatSidePanelWebView::~ChatSidePanelWebView() = default; + +BEGIN_METADATA(ChatSidePanelWebView) +END_METADATA diff --git a/chrome/browser/ui/views/side_panel/chat/chat_side_panel_web_view.h b/chrome/browser/ui/views/side_panel/chat/chat_side_panel_web_view.h new file mode 100644 index 00000000000000..51878db9f7707d --- /dev/null +++ b/chrome/browser/ui/views/side_panel/chat/chat_side_panel_web_view.h @@ -0,0 +1,52 @@ +#ifndef CHROMIUM_CHAT_SIDE_PANEL_WEB_VIEW_H +#define CHROMIUM_CHAT_SIDE_PANEL_WEB_VIEW_H + +#include +#include +#include + +#include "base/functional/callback_forward.h" +#include "base/memory/raw_ptr.h" +#include "chrome/browser/ui/tabs/tab_strip_model_observer.h" +#include "chrome/browser/ui/views/side_panel/side_panel_web_ui_view.h" +#include "chrome/browser/ui/webui/side_panel/chat/chat.mojom.h" +#include "chrome/browser/ui/webui/side_panel/chat/chat_ui.h" +#include "ui/base/metadata/metadata_header_macros.h" +#include "ui/views/controls/webview/webview.h" +#include "ui/views/view.h" +#include "chrome/common/chat/page_content_extractor.mojom.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/bindings/remote.h" + +class Browser; + +class ChatSidePanelWebView : public SidePanelWebUIViewT, + public TabStripModelObserver { + using SidePanelWebUIViewT_ChatUI = SidePanelWebUIViewT; + METADATA_HEADER(ChatSidePanelWebView, SidePanelWebUIViewT_ChatUI) + public: + ChatSidePanelWebView(Browser* browser, base::RepeatingClosure close_cb); + ChatSidePanelWebView(const ChatSidePanelWebView&) = delete; + ChatSidePanelWebView& operator=(const ChatSidePanelWebView&) = delete; + ~ChatSidePanelWebView() override; + + void OnTabStripModelChanged( + TabStripModel* tab_strip_model, + const TabStripModelChange& change, + const TabStripSelectionChange& selection) override; + + void TabChangedAt(content::WebContents* contents, + int index, + TabChangeType change_type) override; + + void UpdateActiveSiteInfo(content::WebContents* contents); + void UpdateActiveWebContents(); + base::WeakPtr GetWeakPtr(); + + private: + const raw_ptr browser_; + base::WeakPtrFactory weak_ptr_factory_{this}; +}; +#endif // CHROMIUM_CHAT_SIDE_PANEL_WEB_VIEW_H diff --git a/chrome/browser/ui/views/side_panel/side_panel.cc b/chrome/browser/ui/views/side_panel/side_panel.cc index 3b33773f39f339..12a528957e1e0c 100644 --- a/chrome/browser/ui/views/side_panel/side_panel.cc +++ b/chrome/browser/ui/views/side_panel/side_panel.cc @@ -61,7 +61,7 @@ int GetBorderThickness() { // This is how many units of the toolbar are essentially expected to be // background. -constexpr int kOverlapFromToolbar = 4; +constexpr int kOverlapFromToolbar = -1; // We want the border to visually look like GetBorderThickness() units on all // sides. On the top side, background is drawn on top of the top-content @@ -71,7 +71,7 @@ gfx::Insets GetBorderInsets() { int border_thickness = GetBorderThickness(); return gfx::Insets::TLBR( border_thickness - views::Separator::kThickness - kOverlapFromToolbar, - border_thickness, border_thickness, border_thickness); + border_thickness, -border_thickness, border_thickness); } constexpr int kAnimationDurationMs = 450; @@ -104,9 +104,7 @@ class SidePanelBorder : public views::Border { view.GetLocalBounds(), view.layer()->device_scale_factor()); gfx::RectF scaled_contents_bounds_f = scaled_view_bounds_f; - const float corner_radius = - dsf * view.GetLayoutProvider()->GetCornerRadiusMetric( - views::ShapeContextTokens::kSidePanelContentRadius); + const float corner_radius = 16; const gfx::InsetsF insets_in_pixels( gfx::ConvertInsetsToPixels(GetInsets(), dsf)); scaled_contents_bounds_f.Inset(insets_in_pixels); @@ -142,6 +140,7 @@ class SidePanelBorder : public views::Border { // border. This is done in DIPs because for some device scale factors, the // conversion to pixels can cause the clip to be off by a pixel, resulting // in a pixel gap between the side panel border and web contents. + canvas->ClipPath(rounded_border_path, /*do_anti_alias=*/true); // Draw the top-container background. @@ -150,12 +149,10 @@ class SidePanelBorder : public views::Border { // Paint the inner border around SidePanel content. Since half the stroke // gets painted in the clipped area, make this twice as thick. - const float stroke_thickness = views::Separator::kThickness * 2; - + const float stroke_thickness = views::Separator::kThickness * 4; cc::PaintFlags flags; flags.setStrokeWidth(stroke_thickness); - flags.setColor( - view.GetColorProvider()->GetColor(kColorSidePanelContentAreaSeparator)); + flags.setColor(view.GetColorProvider()->GetColor(kColorYepChatPanelBorder)); flags.setStyle(cc::PaintFlags::kStroke_Style); flags.setAntiAlias(true); @@ -170,8 +167,9 @@ class SidePanelBorder : public views::Border { // header to paint on top of the border area. int top_inset = views::Separator::kThickness + header_height_ - GetBorderThickness(); - return GetBorderInsets() + gfx::Insets::TLBR(top_inset, 0, 0, 0); + return GetBorderInsets() + gfx::Insets::TLBR(top_inset, 0, 0, 2); } + gfx::Size GetMinimumSize() const override { return gfx::Size(GetInsets().width(), GetInsets().height()); } @@ -235,8 +233,8 @@ class ContentParentView : public views::View { public: ContentParentView() { SetUseDefaultFillLayout(true); - SetBackground( - views::CreateThemedSolidBackground(kColorSidePanelBackground)); + SetBackground(views::CreateThemedSolidBackground( + /*kColorSidePanelBackground*/ kColorTabGroupDialogRed)); SetProperty( views::kFlexBehaviorKey, views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToZero, @@ -416,10 +414,11 @@ void SidePanel::AddHeaderView(std::unique_ptr view) { RemoveChildView(header_view_); } header_view_ = view.get(); + header_view_->SetVisible(false); AddChildView(std::move(view)); static_cast(border_view_)->HeaderViewChanged(header_view_); - // Update the border so that the insets include space for the header to be - // placed on top of the border. + // // Update the border so that the insets include space for the header to be + // // placed on top of the border. int top_inset = header_view_->height() - GetBorderThickness(); SetBorder(views::CreateEmptyBorder(GetBorderInsets() + gfx::Insets::TLBR(top_inset, 0, 0, 0))); diff --git a/chrome/browser/ui/views/side_panel/side_panel_coordinator.cc b/chrome/browser/ui/views/side_panel/side_panel_coordinator.cc index 56029058ace006..062b0a7c76a23f 100644 --- a/chrome/browser/ui/views/side_panel/side_panel_coordinator.cc +++ b/chrome/browser/ui/views/side_panel/side_panel_coordinator.cc @@ -143,40 +143,40 @@ std::unique_ptr CreateControlButton( return button; } -std::unique_ptr CreateIcon() { - std::unique_ptr icon = std::make_unique(); - const int horizontal_margin = - ChromeLayoutProvider::Get()->GetDistanceMetric( - ChromeDistanceMetric:: - DISTANCE_SIDE_PANEL_HEADER_INTERIOR_MARGIN_HORIZONTAL) * - 2; - icon->SetProperty(views::kMarginsKey, - gfx::Insets().set_left(horizontal_margin)); - return icon; -} - -std::unique_ptr CreateTitle() { - std::unique_ptr title = std::make_unique( - std::u16string(), views::style::CONTEXT_LABEL, - views::style::STYLE_HEADLINE_5); - - title->SetEnabledColorId(kColorSidePanelEntryTitle); - title->SetSubpixelRenderingEnabled(false); - const int horizontal_margin = - ChromeLayoutProvider::Get()->GetDistanceMetric( - ChromeDistanceMetric:: - DISTANCE_SIDE_PANEL_HEADER_INTERIOR_MARGIN_HORIZONTAL) * - 2; - title->SetProperty(views::kMarginsKey, - gfx::Insets().set_left(horizontal_margin)); - title->SetProperty( - views::kFlexBehaviorKey, - views::FlexSpecification(views::LayoutOrientation::kHorizontal, - views::MinimumFlexSizeRule::kScaleToZero, - views::MaximumFlexSizeRule::kUnbounded) - .WithAlignment(views::LayoutAlignment::kStart)); - return title; -} +// std::unique_ptr CreateIcon() { +// std::unique_ptr icon = +// std::make_unique(); const int horizontal_margin = +// ChromeLayoutProvider::Get()->GetDistanceMetric( +// ChromeDistanceMetric:: +// DISTANCE_SIDE_PANEL_HEADER_INTERIOR_MARGIN_HORIZONTAL) * +// 2; +// icon->SetProperty(views::kMarginsKey, +// gfx::Insets().set_left(horizontal_margin)); +// return icon; +// } +// +// std::unique_ptr CreateTitle() { +// std::unique_ptr title = std::make_unique( +// std::u16string(), views::style::CONTEXT_LABEL, +// views::style::STYLE_HEADLINE_5); +// +// title->SetEnabledColorId(kColorSidePanelEntryTitle); +// title->SetSubpixelRenderingEnabled(false); +// const int horizontal_margin = +// ChromeLayoutProvider::Get()->GetDistanceMetric( +// ChromeDistanceMetric:: +// DISTANCE_SIDE_PANEL_HEADER_INTERIOR_MARGIN_HORIZONTAL) * +// 2; +// title->SetProperty(views::kMarginsKey, +// gfx::Insets().set_left(horizontal_margin)); +// title->SetProperty( +// views::kFlexBehaviorKey, +// views::FlexSpecification(views::LayoutOrientation::kHorizontal, +// views::MinimumFlexSizeRule::kScaleToZero, +// views::MaximumFlexSizeRule::kUnbounded) +// .WithAlignment(views::LayoutAlignment::kStart)); +// return title; +// } using PopulateSidePanelCallback = base::OnceCallbackIsActionPinned(*extension_id); actions_model->SetActionVisibility(*extension_id, updated_pin_state); } else { - PinnedToolbarActionsModel* const actions_model = - PinnedToolbarActionsModel::Get(profile); - - updated_pin_state = !actions_model->Contains(action_id.value()); + PinnedToolbarActionsModel* const actions_model = PinnedToolbarActionsModel::Get(profile); + if (current_entry_->key().id() == SidePanelEntryId::kAIChat) { + updated_pin_state = false; + } else { + updated_pin_state = !actions_model->Contains(action_id.value()); + } actions_model->UpdatePinnedState(action_id.value(), updated_pin_state); } @@ -713,11 +715,11 @@ std::unique_ptr SidePanelCoordinator::CreateHeader() { layout->SetCrossAxisAlignment(views::LayoutAlignment::kCenter); // The minimum cross axis size should the expected height of the header. - constexpr int kDefaultSidePanelHeaderHeight = 40; + constexpr int kDefaultSidePanelHeaderHeight = 0; layout->SetMinimumCrossAxisSize(kDefaultSidePanelHeaderHeight); - panel_icon_ = header->AddChildView(CreateIcon()); - panel_title_ = header->AddChildView(CreateTitle()); + // panel_icon_ = header->AddChildView(CreateIcon()); + // panel_title_ = header->AddChildView(CreateTitle()); header_pin_button_ = header->AddChildView(CreatePinToggleButton(base::BindRepeating( @@ -767,15 +769,15 @@ std::unique_ptr SidePanelCoordinator::CreateHeader() { std::make_unique( header_more_info_button_))); - auto* header_close_button = header->AddChildView(CreateControlButton( - header.get(), - base::BindRepeating(&SidePanelUI::Close, base::Unretained(this)), - views::kIcCloseIcon, - l10n_util::GetStringUTF16(IDS_ACCNAME_SIDE_PANEL_CLOSE), - kSidePanelCloseButtonElementId, - ChromeLayoutProvider::Get()->GetDistanceMetric( - ChromeDistanceMetric::DISTANCE_SIDE_PANEL_HEADER_VECTOR_ICON_SIZE))); - header_close_button->SetFocusBehavior(views::View::FocusBehavior::ALWAYS); + // auto* header_close_button = header->AddChildView(CreateControlButton( + // header.get(), + // base::BindRepeating(&SidePanelUI::Close, base::Unretained(this)), + // views::kIcCloseIcon, + // l10n_util::GetStringUTF16(IDS_ACCNAME_SIDE_PANEL_CLOSE), + // kSidePanelCloseButtonElementId, + // ChromeLayoutProvider::Get()->GetDistanceMetric( + // ChromeDistanceMetric::DISTANCE_SIDE_PANEL_HEADER_VECTOR_ICON_SIZE))); + // header_close_button->SetFocusBehavior(views::View::FocusBehavior::ALWAYS); return header; } @@ -835,6 +837,9 @@ void SidePanelCoordinator::NotifyPinnedContainerOfActiveStateChange( std::optional action_id = SidePanelEntryIdToActionId(key.id()); CHECK(action_id.has_value()); + if (key.id() == SidePanelEntryId::kAIChat) { + is_active = false; + } toolbar_container->UpdateActionState(*action_id, is_active); } } @@ -1046,10 +1051,16 @@ void SidePanelCoordinator::UpdatePanelIconAndTitle( *icon.GetVectorIcon().vector_icon(), kColorSidePanelEntryIcon, icon.GetVectorIcon().icon_size()); } - panel_icon_->SetImage(updated_icon); + if (panel_icon_) { + panel_icon_->SetImage(updated_icon); + } + } + if (panel_icon_) { + panel_icon_->SetVisible(is_extension); + } + if (panel_title_) { + panel_title_->SetText(should_show_title_text ? text : std::u16string()); } - panel_icon_->SetVisible(is_extension); - panel_title_->SetText(should_show_title_text ? text : std::u16string()); } void SidePanelCoordinator::OnViewVisibilityChanged(views::View* observed_view, @@ -1103,7 +1114,7 @@ void SidePanelCoordinator::OnViewVisibilityChanged(views::View* observed_view, if (!content_wrapper->children().empty()) { content_wrapper->RemoveChildViewT(content_wrapper->children().front()); } - SidePanelUtil::RecordSidePanelClosed(opened_timestamp_); + SidePanelUtil::RecordSidePanelClosed(browser_view_->browser(), previous_entry->key().id(), opened_timestamp_); view_state_observers_.Notify( &SidePanelViewStateObserver::OnSidePanelDidClose); diff --git a/chrome/browser/ui/views/side_panel/side_panel_entry_id.h b/chrome/browser/ui/views/side_panel/side_panel_entry_id.h index fe3eaaa62ebf4b..1cb52752d07bf7 100644 --- a/chrome/browser/ui/views/side_panel/side_panel_entry_id.h +++ b/chrome/browser/ui/views/side_panel/side_panel_entry_id.h @@ -19,6 +19,7 @@ // since we cannot autogenerate this in actions.xml. #define SIDE_PANEL_ENTRY_IDS(V) \ /* Global Entries */ \ + V(kAIChat, kActionAIChat, "AIChat") \ V(kReadingList, kActionSidePanelShowReadingList, "ReadingList") \ V(kBookmarks, kActionSidePanelShowBookmarks, "Bookmarks") \ V(kHistoryClusters, kActionSidePanelShowHistoryCluster, "HistoryClusters") \ diff --git a/chrome/browser/ui/views/side_panel/side_panel_util.cc b/chrome/browser/ui/views/side_panel/side_panel_util.cc index 037c5c34e68cf7..a635094c1f2589 100644 --- a/chrome/browser/ui/views/side_panel/side_panel_util.cc +++ b/chrome/browser/ui/views/side_panel/side_panel_util.cc @@ -16,7 +16,9 @@ #include "chrome/browser/ui/browser_window/public/browser_window_features.h" #include "chrome/browser/ui/ui_features.h" #include "chrome/browser/ui/views/side_panel/bookmarks/bookmarks_side_panel_coordinator.h" +#include "chrome/browser/ui/views/side_panel/chat/chat_side_panel_coordinator.h" #include "chrome/browser/ui/views/side_panel/companion/companion_utils.h" +#include "chrome/browser/ui/views/side_panel/extensions/extension_side_panel_manager.h" #include "chrome/browser/ui/views/side_panel/history_clusters/history_clusters_side_panel_coordinator.h" #include "chrome/browser/ui/views/side_panel/reading_list/reading_list_side_panel_coordinator.h" #include "chrome/browser/ui/views/side_panel/search_companion/search_companion_side_panel_coordinator.h" @@ -30,7 +32,6 @@ #include "components/user_notes/user_notes_features.h" #include "ui/accessibility/accessibility_features.h" #include "ui/actions/actions.h" -#include "chrome/browser/ui/views/side_panel/extensions/extension_side_panel_manager.h" // static void SidePanelUtil::PopulateGlobalEntries(Browser* browser, @@ -39,6 +40,10 @@ void SidePanelUtil::PopulateGlobalEntries(Browser* browser, ReadingListSidePanelCoordinator::GetOrCreateForBrowser(browser) ->CreateAndRegisterEntry(window_registry); + // Add ai chat + ChatSidePanelCoordinator::GetOrCreateForBrowser(browser) + ->CreateAndRegisterEntry(window_registry); + // Add bookmarks. BookmarksSidePanelCoordinator::GetOrCreateForBrowser(browser) ->CreateAndRegisterEntry(window_registry); @@ -87,11 +92,17 @@ void SidePanelUtil::RecordSidePanelShowOrChangeEntryTrigger( } } -void SidePanelUtil::RecordSidePanelClosed(base::TimeTicks opened_timestamp) { +void SidePanelUtil::RecordSidePanelClosed(Browser* browser, + SidePanelEntry::Id id, + base::TimeTicks opened_timestamp) { base::RecordAction(base::UserMetricsAction("SidePanel.Hide")); base::UmaHistogramLongTimes("SidePanel.OpenDuration", base::TimeTicks::Now() - opened_timestamp); + + auto* ai_chat_coordinator = + ChatSidePanelCoordinator::GetOrCreateForBrowser(browser); + ai_chat_coordinator->UpdateClosingPanelId(id); } void SidePanelUtil::RecordSidePanelResizeMetrics(SidePanelEntry::Id id, @@ -147,6 +158,10 @@ void SidePanelUtil::RecordEntryShowTriggeredMetrics( Browser* browser, SidePanelEntry::Id id, std::optional trigger) { + auto* ai_chat_coordinator = + ChatSidePanelCoordinator::GetOrCreateForBrowser(browser); + ai_chat_coordinator->UpdateOpeningPanelId(id); + if (trigger.has_value()) { base::UmaHistogramEnumeration( base::StrCat({"SidePanel.", SidePanelEntryIdToHistogramName(id), diff --git a/chrome/browser/ui/views/side_panel/side_panel_util.h b/chrome/browser/ui/views/side_panel/side_panel_util.h index 334f193376a4a4..eb56c3b53a79f4 100644 --- a/chrome/browser/ui/views/side_panel/side_panel_util.h +++ b/chrome/browser/ui/views/side_panel/side_panel_util.h @@ -39,7 +39,9 @@ class SidePanelUtil { static void RecordSidePanelOpen(std::optional trigger); static void RecordSidePanelShowOrChangeEntryTrigger( std::optional trigger); - static void RecordSidePanelClosed(base::TimeTicks opened_timestamp); + static void RecordSidePanelClosed(Browser* browser, + SidePanelEntry::Id id, + base::TimeTicks opened_timestamp); static void RecordSidePanelResizeMetrics(SidePanelEntry::Id id, int side_panel_contents_width, int browser_window_width); diff --git a/chrome/browser/ui/views/toolbar/BUILD.gn b/chrome/browser/ui/views/toolbar/BUILD.gn index c322ee82949f27..dea8aeb060f358 100644 --- a/chrome/browser/ui/views/toolbar/BUILD.gn +++ b/chrome/browser/ui/views/toolbar/BUILD.gn @@ -60,6 +60,8 @@ source_set("toolbar") { "toolbar_ink_drop_util.h", "toolbar_view.cc", "toolbar_view.h", + "ai_chat_toolbar_button.cc", + "ai_chat_toolbar_button.h" ] public_deps = [ "//base", diff --git a/chrome/browser/ui/views/toolbar/ai_chat_toolbar_button.cc b/chrome/browser/ui/views/toolbar/ai_chat_toolbar_button.cc new file mode 100644 index 00000000000000..778e899ee68908 --- /dev/null +++ b/chrome/browser/ui/views/toolbar/ai_chat_toolbar_button.cc @@ -0,0 +1,34 @@ +#include "ai_chat_toolbar_button.h" + +#include "chrome/app/vector_icons/vector_icons.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/view_ids.h" +#include "chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.h" +#include "chrome/grit/generated_resources.h" +#include "components/vector_icons/vector_icons.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/base/metadata/metadata_header_macros.h" +#include "ui/base/metadata/metadata_impl_macros.h" +#include "ui/views/view.h" + +AIChatToolbarButton::AIChatToolbarButton(PressedCallback callback) + : ToolbarButton(std::move(callback)) { + SetTooltipText(l10n_util::GetStringUTF16(IDS_AI_CHAT_TITLE)); + SetHorizontalAlignment(gfx::ALIGN_CENTER); + SetVectorIcon(kChatIcon); + SetVisible(true); + ConfigureInkDropForToolbar(this); +} + +AIChatToolbarButton::~AIChatToolbarButton() = default; + +void AIChatToolbarButton::AddHighlight() { + anchor_higlight_ = AddAnchorHighlight(); +} + +void AIChatToolbarButton::ResetHighlight() { + anchor_higlight_.reset(); +} + +BEGIN_METADATA(AIChatToolbarButton) +END_METADATA diff --git a/chrome/browser/ui/views/toolbar/ai_chat_toolbar_button.h b/chrome/browser/ui/views/toolbar/ai_chat_toolbar_button.h new file mode 100644 index 00000000000000..7cad8463a7bc06 --- /dev/null +++ b/chrome/browser/ui/views/toolbar/ai_chat_toolbar_button.h @@ -0,0 +1,26 @@ +#ifndef CHROMIUM_AI_CHAT_TOOLBAR_BUTTON_H +#define CHROMIUM_AI_CHAT_TOOLBAR_BUTTON_H + +#include "base/memory/raw_ptr.h" +#include "chrome/browser/ui/views/toolbar/toolbar_button.h" +#include "ui/base/metadata/metadata_header_macros.h" + +class Browser; + +class AIChatToolbarButton : public ToolbarButton { + METADATA_HEADER(AIChatToolbarButton, ToolbarButton) + + public: + AIChatToolbarButton(PressedCallback callback); + AIChatToolbarButton(const AIChatToolbarButton&) = delete; + AIChatToolbarButton& operator=(const AIChatToolbarButton&) = delete; + ~AIChatToolbarButton() override; + + void AddHighlight(); + void ResetHighlight(); + + private: + std::optional anchor_higlight_; +}; + +#endif //CHROMIUM_AI_CHAT_TOOLBAR_BUTTON_H \ No newline at end of file diff --git a/chrome/browser/ui/views/toolbar/browser_app_menu_button.cc b/chrome/browser/ui/views/toolbar/browser_app_menu_button.cc index 5300d5a128c80d..0633f7c3a370b4 100644 --- a/chrome/browser/ui/views/toolbar/browser_app_menu_button.cc +++ b/chrome/browser/ui/views/toolbar/browser_app_menu_button.cc @@ -151,7 +151,7 @@ void BrowserAppMenuButton::UpdateThemeBasedState() { void BrowserAppMenuButton::UpdateIcon() { const gfx::VectorIcon& icon = ui::TouchUiController::Get()->touch_ui() ? kBrowserToolsTouchIcon - : kBrowserToolsChromeRefreshIcon; + : kMoreHorizIcon; for (auto state : kButtonStates) { SkColor icon_color = GetForegroundColor(state); SetImageModel(state, ui::ImageModel::FromVectorIcon(icon, icon_color)); diff --git a/chrome/browser/ui/views/toolbar/toolbar_button.cc b/chrome/browser/ui/views/toolbar/toolbar_button.cc index 1328a7f6277005..75cbe02f6b7f1d 100644 --- a/chrome/browser/ui/views/toolbar/toolbar_button.cc +++ b/chrome/browser/ui/views/toolbar/toolbar_button.cc @@ -162,7 +162,7 @@ void ToolbarButton::ClearHighlight() { int ToolbarButton::GetRoundedCornerRadius() const { return ChromeLayoutProvider::Get()->GetCornerRadiusMetric( - views::Emphasis::kMaximum, GetTargetSize()); + views::Emphasis::kHigh, GetTargetSize()); } float ToolbarButton::GetCornerRadiusFor(ToolbarButton::Edge edge) const { @@ -253,12 +253,16 @@ void ToolbarButton::UpdateIconsWithColors(const gfx::VectorIcon& icon, SkColor pressed_color, SkColor disabled_color) { const int icon_size = GetIconSize(); + + // todo: to check alpha value with Yuri when UI customization starts + const int alpha = 150; + SetImageModel(ButtonState::STATE_NORMAL, - ui::ImageModel::FromVectorIcon(icon, normal_color, icon_size)); + ui::ImageModel::FromVectorIcon(icon, SkColorSetA(normal_color, alpha), icon_size)); SetImageModel(ButtonState::STATE_HOVERED, - ui::ImageModel::FromVectorIcon(icon, hovered_color, icon_size)); + ui::ImageModel::FromVectorIcon(icon, SkColorSetA( hovered_color, alpha), icon_size)); SetImageModel(ButtonState::STATE_PRESSED, - ui::ImageModel::FromVectorIcon(icon, pressed_color, icon_size)); + ui::ImageModel::FromVectorIcon(icon, SkColorSetA(pressed_color, alpha), icon_size)); SetImageModel(Button::STATE_DISABLED, ui::ImageModel::FromVectorIcon( icon, disabled_color, icon_size)); } diff --git a/chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.cc b/chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.cc index e6c3026e63cd85..9c26c0dd88edee 100644 --- a/chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.cc +++ b/chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.cc @@ -40,10 +40,11 @@ class ToolbarButtonHighlightPathGenerator rect.Inset(GetToolbarInkDropInsets(view)); const int radii = ChromeLayoutProvider::Get()->GetCornerRadiusMetric( - views::Emphasis::kMaximum, rect.size()); + views::Emphasis::kHigh, rect.size() + ); SkPath path; - path.addRoundRect(gfx::RectToSkRect(rect), radii, radii); + path.addRoundRect(gfx::RectToSkRect( gfx::Rect( rect)), radii, radii); return path; } }; diff --git a/chrome/browser/ui/views/toolbar/toolbar_view.cc b/chrome/browser/ui/views/toolbar/toolbar_view.cc index 1b494d20b63426..a528d03fdafa0b 100644 --- a/chrome/browser/ui/views/toolbar/toolbar_view.cc +++ b/chrome/browser/ui/views/toolbar/toolbar_view.cc @@ -123,6 +123,14 @@ #include "ui/views/widget/tooltip_manager.h" #include "ui/views/widget/widget.h" #include "ui/views/window/non_client_view.h" +#include "chrome/browser/ui/views/side_panel/side_panel_coordinator.h" +#include "chrome/browser/ui/views/side_panel/side_panel_entry_key.h" +#include "chrome/browser/ui/views/side_panel/side_panel_enums.h" +#include "chrome/browser/ui/views/side_panel/side_panel_entry_id.h" +#include "chrome/browser/ui/views/side_panel/side_panel_ui.h" +#include "ui/views/background.h" +#include "ui/views/view.h" +#include "ui/gfx/color_palette.h" #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) #include "chrome/browser/recovery/recovery_install_global_error_factory.h" @@ -352,6 +360,10 @@ void ToolbarView::Init() { std::unique_ptr reload = std::make_unique(browser_->command_controller()); + std::unique_ptr ai_chat_button = std::make_unique( + base::BindRepeating( + &ToolbarView::AIChatButtonPressed, base::Unretained(this))); + PrefService* const prefs = browser_->profile()->GetPrefs(); std::unique_ptr home = std::make_unique( base::BindRepeating(callback, browser_, IDC_HOME), prefs); @@ -506,11 +518,12 @@ void ToolbarView::Init() { #endif // BUILDFLAG(ENABLE_WEBUI_TAB_STRIP) if (base::FeatureList::IsEnabled(features::kResponsiveToolbar)) { - overflow_button_ = - container_view_->AddChildView(std::make_unique()); - overflow_button_->SetVisible(false); + overflow_button_ = container_view_->AddChildView(std::make_unique()); + overflow_button_->SetVisible(false); } + ai_chat_button_ = container_view_->AddChildView(std::move(ai_chat_button)); + auto app_menu_button = std::make_unique(this); app_menu_button->SetFlipCanvasOnPaintForRTLUI(true); app_menu_button->GetViewAccessibility().SetName( @@ -559,6 +572,14 @@ void ToolbarView::Init() { initialized_ = true; } +void ToolbarView::ResetHighlightForAIChatButton() { + ai_chat_button_->ResetHighlight(); +} + +void ToolbarView::AddHighlightForAIChatButton() { + ai_chat_button_->AddHighlight(); +} + void ToolbarView::AnimationEnded(const gfx::Animation* animation) { if (animation->GetCurrentValue() == 0) SetToolbarVisibility(false); @@ -932,6 +953,18 @@ void ToolbarView::NewTabButtonPressed(const ui::Event& event) { NewTabTypes::NEW_TAB_ENUM_COUNT); } +void ToolbarView::AIChatButtonPressed(const ui::Event& event) { + is_ai_chat_button_active_ = !is_ai_chat_button_active_; + if (is_ai_chat_button_active_) { + ai_chat_button_->AddHighlight(); + } else { + ai_chat_button_->ResetHighlight(); + } + auto key = SidePanelEntryKey(SidePanelEntryId::kAIChat); + auto *side_panel = browser_view_ -> browser()->GetFeatures().side_panel_ui(); + side_panel->Toggle(key, SidePanelOpenTrigger::kToolbarButton); +} + bool ToolbarView::AcceleratorPressed(const ui::Accelerator& accelerator) { const views::View* focused_view = focus_manager()->GetFocusedView(); if (focused_view && (focused_view->GetID() == VIEW_ID_OMNIBOX)) diff --git a/chrome/browser/ui/views/toolbar/toolbar_view.h b/chrome/browser/ui/views/toolbar/toolbar_view.h index b02224437ad20b..48ea7acc62bbb1 100644 --- a/chrome/browser/ui/views/toolbar/toolbar_view.h +++ b/chrome/browser/ui/views/toolbar/toolbar_view.h @@ -24,6 +24,7 @@ #include "chrome/browser/ui/views/location_bar/location_bar_view.h" #include "chrome/browser/ui/views/profiles/avatar_toolbar_button.h" #include "chrome/browser/ui/views/toolbar/overflow_button.h" +#include "chrome/browser/ui/views/toolbar/ai_chat_toolbar_button.h" #include "components/prefs/pref_member.h" #include "ui/base/accelerators/accelerator.h" #include "ui/base/metadata/metadata_header_macros.h" @@ -217,6 +218,9 @@ class ToolbarView : public views::AccessiblePaneView, bool AcceleratorPressed(const ui::Accelerator& acc) override; void ChildPreferredSizeChanged(views::View* child) override; + void ResetHighlightForAIChatButton(); + void AddHighlightForAIChatButton(); + friend class AvatarToolbarButtonBrowserTest; protected: @@ -289,6 +293,10 @@ class ToolbarView : public views::AccessiblePaneView, void NewTabButtonPressed(const ui::Event& event); + + bool is_ai_chat_button_active_ = false; + void AIChatButtonPressed(const ui::Event& event); + gfx::SlideAnimation size_animation_{this}; // Controls. Most of these can be null, e.g. in popup windows. Only @@ -362,6 +370,8 @@ class ToolbarView : public views::AccessiblePaneView, // `toolbar_controller_`. raw_ptr overflow_button_ = nullptr; + raw_ptr ai_chat_button_ = nullptr; + // There are two situations where background_view_left_ and // background_view_right_ need be repainted: window active state change and // theme change. active_state_subscription_ handles the former, and the latter @@ -372,6 +382,7 @@ class ToolbarView : public views::AccessiblePaneView, // Listens to changes to active state to update background_view_right_ and // background_view_left_, as their background depends on active state. base::CallbackListSubscription active_state_subscription_; + }; extern const ui::ClassProperty* const kActionItemUnderlineIndicatorKey; diff --git a/chrome/browser/ui/webui/chrome_web_ui_configs.cc b/chrome/browser/ui/webui/chrome_web_ui_configs.cc index 6d46b2bea8b206..cfe958a58fc8e1 100644 --- a/chrome/browser/ui/webui/chrome_web_ui_configs.cc +++ b/chrome/browser/ui/webui/chrome_web_ui_configs.cc @@ -145,6 +145,7 @@ #include "chrome/browser/ui/webui/app_settings/web_app_settings_ui.h" #include "chrome/browser/ui/webui/browser_switch/browser_switch_ui.h" #include "chrome/browser/ui/webui/whats_new/whats_new_ui.h" +#include "chrome/browser/ui/webui/side_panel/chat/chat_ui.h" #endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || \ @@ -252,6 +253,7 @@ void RegisterChromeWebUIConfigs() { map.AddWebUIConfig( std::make_unique< privacy_sandbox_internals::PrivacySandboxInternalsUIConfig>()); + map.AddWebUIConfig(std::make_unique()); #if BUILDFLAG(ENABLE_NACL) map.AddWebUIConfig(std::make_unique()); diff --git a/chrome/browser/ui/webui/side_panel/chat/BUILD.gn b/chrome/browser/ui/webui/side_panel/chat/BUILD.gn new file mode 100644 index 00000000000000..d892a802b58a83 --- /dev/null +++ b/chrome/browser/ui/webui/side_panel/chat/BUILD.gn @@ -0,0 +1,13 @@ +import("//mojo/public/tools/bindings/mojom.gni") + +assert(!is_android) + +mojom("mojo_bindings") { + sources = [ "chat.mojom" ] + webui_module_path = "/" + public_deps = [ + "//mojo/public/mojom/base", + "//ui/base/mojom", + "//url/mojom:url_mojom_gurl", + ] +} \ No newline at end of file diff --git a/chrome/browser/ui/webui/side_panel/chat/api/api_request_helper.cc b/chrome/browser/ui/webui/side_panel/chat/api/api_request_helper.cc new file mode 100644 index 00000000000000..475791ce397856 --- /dev/null +++ b/chrome/browser/ui/webui/side_panel/chat/api/api_request_helper.cc @@ -0,0 +1,541 @@ +#include "api_request_helper.h" + +#include +#include +#include + +#include "base/check.h" +#include "base/check_op.h" +#include "base/debug/alias.h" +#include "base/debug/dump_without_crashing.h" +#include "base/functional/callback_helpers.h" +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/memory/raw_ptr.h" +#include "base/metrics/histogram_functions.h" +#include "base/ranges/algorithm.h" +#include "base/rust_buildflags.h" +#include "base/strings/string_split.h" +#include "base/task/thread_pool.h" +#include "base/time/time.h" +#include "base/timer/elapsed_timer.h" +#include "base/trace_event/trace_event.h" +#include "net/base/load_flags.h" +#include "net/http/http_status_code.h" +#include "services/data_decoder/public/cpp/data_decoder.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "services/network/public/mojom/url_response_head.mojom.h" + +namespace api_request_helper { + + namespace { + + const unsigned int kRetriesCountOnNetworkChange = 1; + + scoped_refptr MakeDecoderTaskRunner() { + return base::ThreadPool::CreateSequencedTaskRunner( + {base::TaskPriority::USER_VISIBLE, + base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}); + } + + APIRequestResult ToAPIRequestResult( + std::unique_ptr loader) { + auto response_code = -1; + auto error_code = loader->NetError(); + auto final_url = loader->GetFinalURL(); + base::flat_map headers; + if (loader->ResponseInfo()) { + auto headers_list = loader->ResponseInfo()->headers; + if (headers_list) { + response_code = headers_list->response_code(); + DVLOG(1) << "Response code: " << response_code; + size_t header_iter = 0; + std::string key; + std::string value; + while (headers_list->EnumerateHeaderLines(&header_iter, &key, &value)) { + key = base::ToLowerASCII(key); + headers[key] = value; + DVLOG(2) << "< " << key << ": " << value; + } + } + } + + return APIRequestResult(response_code, base::Value(), std::move(headers), + error_code, final_url); + } + + } // namespace + + APIRequestResult::APIRequestResult() = default; + + APIRequestResult::APIRequestResult( + int response_code, + base::Value value_body, + base::flat_map headers, + int error_code, + GURL final_url) + : response_code_(response_code), + value_body_(std::move(value_body)), + headers_(std::move(headers)), + error_code_(error_code), + final_url_(std::move(final_url)) {} + + APIRequestResult::APIRequestResult(APIRequestResult&&) = default; + + APIRequestResult& APIRequestResult::operator=(APIRequestResult&&) = default; + + APIRequestResult::~APIRequestResult() = default; + + bool APIRequestResult::operator==(const APIRequestResult& other) const { + auto tied = [](auto& v) { + return std::tie(v.response_code_, v.value_body_, v.headers_, v.error_code_, + v.final_url_); + }; + return tied(*this) == tied(other); + } + + bool APIRequestResult::operator!=(const APIRequestResult& other) const { + return !(*this == other); + } + + bool APIRequestResult::Is2XXResponseCode() const { + return response_code_ >= 200 && response_code_ <= 299; + } + + bool APIRequestResult::IsResponseCodeValid() const { + return response_code_ >= 100 && response_code_ <= 599; + } + + base::Value APIRequestResult::TakeBody() { + CHECK(!body_consumed_); + body_consumed_ = true; + return std::move(value_body_); + } + + std::string APIRequestResult::SerializeBodyToString() const { + if (value_body_.is_none()) { + return std::string(); + } + std::string safe_json; + if (!base::JSONWriter::Write(value_body_, &safe_json)) { + VLOG(1) << "Response validation error: Encoding error"; + } + + return safe_json; + } + + APIRequestHelper::APIRequestHelper( + net::NetworkTrafficAnnotationTag annotation_tag, + scoped_refptr url_loader_factory) + : annotation_tag_(annotation_tag), + url_loader_factory_(url_loader_factory), + task_runner_(MakeDecoderTaskRunner()) {} + + APIRequestHelper::~APIRequestHelper() = default; + + APIRequestHelper::Ticket APIRequestHelper::Request( + const std::string& method, + const GURL& url, + const std::string& payload, + const std::string& payload_content_type, + ResultCallback callback, + const base::flat_map& headers, + const APIRequestOptions& request_options, + ResponseConversionCallback conversion_callback) { + auto iter = CreateRequestURLLoaderHandler( + method, url, payload, payload_content_type, request_options, headers, + std::move(callback)); + auto* handler = iter->get(); + + if (request_options.max_body_size == -1u) { + handler->url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie( + url_loader_factory_.get(), + base::BindOnce(&APIRequestHelper::URLLoaderHandler::OnResponse, + handler->GetWeakPtr(), std::move(conversion_callback))); + } else { + handler->url_loader_->DownloadToString( + url_loader_factory_.get(), + base::BindOnce(&APIRequestHelper::URLLoaderHandler::OnResponse, + handler->GetWeakPtr(), std::move(conversion_callback)), + request_options.max_body_size); + } + + return iter; + } + + APIRequestHelper::Ticket APIRequestHelper::RequestSSE( + const std::string& method, + const GURL& url, + const std::string& payload, + const std::string& payload_content_type, + DataReceivedCallback data_received_callback, + ResultCallback result_callback, + const base::flat_map& headers, + const APIRequestOptions& request_options) { + return RequestSSE(method, url, payload, payload_content_type, + std::move(data_received_callback), + std::move(result_callback), headers, request_options, + base::NullCallback()); + } + + APIRequestHelper::Ticket APIRequestHelper::RequestSSE( + const std::string& method, + const GURL& url, + const std::string& payload, + const std::string& payload_content_type, + DataReceivedCallback data_received_callback, + ResultCallback result_callback, + const base::flat_map& headers, + const APIRequestOptions& request_options, + ResponseStartedCallback response_started_callback) { + auto iter = CreateRequestURLLoaderHandler( + method, url, payload, payload_content_type, request_options, headers, + std::move(result_callback)); + auto* handler = iter->get(); + + // Set streaming data callback + handler->data_received_callback_ = std::move(data_received_callback); + + handler->response_started_callback_ = std::move(response_started_callback); + + handler->url_loader_->DownloadAsStream(url_loader_factory_.get(), handler); + return iter; + } + + void APIRequestHelper::DeleteAndSendResult(Ticket iter, + ResultCallback callback, + APIRequestResult result) { + Cancel(iter); + std::move(callback).Run(std::move(result)); + } + + void APIRequestHelper::Cancel(const Ticket& ticket) { + url_loaders_.erase(ticket); + } + + void APIRequestHelper::CancelAll() { + url_loaders_.clear(); + } + + APIRequestHelper::Ticket APIRequestHelper::CreateURLLoaderHandler( + const std::string& method, + const GURL& url, + const std::string& payload, + const std::string& payload_content_type, + bool auto_retry_on_network_change, + bool enable_cache, + bool allow_http_error_result, + const base::flat_map& headers) { + auto request = std::make_unique(); + request->url = url; + request->load_flags = net::LOAD_DO_NOT_SAVE_COOKIES; + if (!enable_cache) { + request->load_flags = + request->load_flags | net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE; + } + + request->credentials_mode = network::mojom::CredentialsMode::kOmit; + if (!method.empty()) { + request->method = method; + } + + DVLOG(0) << method << " " << url.spec(); + + if (!headers.empty()) { + for (auto entry : headers) { + DVLOG(4) << "> " << entry.first << ": " << entry.second; + request->headers.SetHeader(entry.first, entry.second); + } + } + + auto url_loader = + network::SimpleURLLoader::Create(std::move(request), annotation_tag_); + if (!payload.empty()) { + url_loader->AttachStringForUpload(payload, payload_content_type); + } + url_loader->SetRetryOptions( + kRetriesCountOnNetworkChange, + auto_retry_on_network_change + ? network::SimpleURLLoader::RetryMode::RETRY_ON_NETWORK_CHANGE + : network::SimpleURLLoader::RetryMode::RETRY_NEVER); + url_loader->SetAllowHttpErrorResults(allow_http_error_result); + + auto loader_wrapper_handler = + std::make_unique(this, task_runner_); + loader_wrapper_handler->RegisterURLLoader(std::move(url_loader)); + + auto iter = url_loaders_.insert(url_loaders_.begin(), + std::move(loader_wrapper_handler)); + + return iter; + } + + APIRequestHelper::Ticket APIRequestHelper::CreateRequestURLLoaderHandler( + const std::string& method, + const GURL& url, + const std::string& payload, + const std::string& payload_content_type, + const APIRequestOptions& request_options, + const base::flat_map& headers, + ResultCallback result_callback) { + auto iter = CreateURLLoaderHandler( + method, url, payload, payload_content_type, + request_options.auto_retry_on_network_change, + request_options.enable_cache, true /* allow_http_error_result*/, headers); + auto* handler = iter->get(); + + handler->result_callback_ = base::BindOnce( + &APIRequestHelper::DeleteAndSendResult, weak_ptr_factory_.GetWeakPtr(), + iter, std::move(result_callback)); + if (request_options.timeout) { + handler->url_loader_->SetTimeoutDuration(request_options.timeout.value()); + } + return iter; + } + + APIRequestHelper::URLLoaderHandler::URLLoaderHandler( + APIRequestHelper* api_request_helper, + scoped_refptr task_runner) + : api_request_helper_(api_request_helper), + previous_invalid_piece_of_response_chunk_(""), + task_runner_(std::move(task_runner)) {} + + APIRequestHelper::URLLoaderHandler::~URLLoaderHandler() = default; + + void APIRequestHelper::URLLoaderHandler::RegisterURLLoader( + std::unique_ptr loader) { + url_loader_ = std::move(loader); + + auto on_response_start = + [](base::WeakPtr handler, + const GURL& final_url, + const network::mojom::URLResponseHead& response_head) { + if (handler) { + if (response_head.mime_type == "text/event-stream") { + handler->is_sse_ = true; + } + if (handler->response_started_callback_) { + std::move(handler->response_started_callback_) + .Run(final_url.spec(), response_head.content_length); + } + } + }; + + url_loader_->SetOnResponseStartedCallback(base::BindOnce( + std::move(on_response_start), weak_ptr_factory_.GetWeakPtr())); + } + + base::WeakPtr + APIRequestHelper::URLLoaderHandler::GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); + } + + void APIRequestHelper::URLLoaderHandler::ParseJsonImpl( + std::string json, + base::OnceCallback callback) { + if (!data_decoder_) { + VLOG(0) << "Creating DataDecoder for APIRequestHelper"; + data_decoder_ = std::make_unique(); + } + + data_decoder_->ParseJson(json, std::move(callback)); + } + + void APIRequestHelper::URLLoaderHandler::OnDataReceived( + std::string_view string_piece, + base::OnceClosure resume) { + DVLOG(0) << "[[" << __func__ << "]]" << " Chunk received"; + if (is_sse_) { + ParseSSE(string_piece); + } else { + DVLOG(0) << "Chunk content: \n" << string_piece; + data_received_callback_.Run(base::Value(string_piece)); + } + std::move(resume).Run(); + } + + void APIRequestHelper::URLLoaderHandler::OnComplete(bool success) { + DCHECK(result_callback_); + VLOG(0) << "[[" << __func__ << "]]" << " Response completed\n"; + + request_is_finished_ = true; + + // Delete now or when decoding operations are complete + MaybeSendResult(); + } + + void APIRequestHelper::URLLoaderHandler::OnRetry( + base::OnceClosure start_retry) { + std::move(start_retry).Run(); + } + + void APIRequestHelper::URLLoaderHandler::OnResponse( + ResponseConversionCallback conversion_callback, + const std::unique_ptr response_body) { + VLOG(0) << "[[" << __func__ << "]]" << " Response received\n"; + DCHECK(result_callback_); + + DCHECK_EQ(current_decoding_operation_count_, 0); + APIRequestResult result = ToAPIRequestResult(std::move(url_loader_)); + + if (!response_body) { + std::move(result_callback_).Run(std::move(result)); + return; + } + auto& raw_body = *response_body; + if (conversion_callback) { + auto converted_body = std::move(conversion_callback).Run(raw_body); + if (!converted_body) { + result.response_code_ = 422; + std::move(result_callback_).Run(std::move(result)); + return; + } + raw_body = converted_body.value(); + } + + ParseJsonImpl( + std::move(raw_body), + base::BindOnce(&APIRequestHelper::URLLoaderHandler::OnParseJsonResponse, + GetWeakPtr(), std::move(result))); + } + + void APIRequestHelper::URLLoaderHandler::OnParseJsonResponse( + APIRequestResult result, + ValueOrError result_value) { + if (!result_value.has_value()) { + VLOG(0) << "Response validation error:" << result_value.error(); + if (result_value.error().starts_with("trailing comma")) { + DEBUG_ALIAS_FOR_GURL(url_alias, result.final_url()); + DEBUG_ALIAS_FOR_CSTR(result_str, result_value.error().c_str(), 1024); + base::debug::DumpWithoutCrashing(); + } + std::move(result_callback_).Run(std::move(result)); + return; + } + if (!result_value.value().is_dict() && !result_value.value().is_list()) { + VLOG(0) << "Response validation error: Invalid top-level type"; + std::move(result_callback_).Run(std::move(result)); + return; + } + + VLOG(2) << "Response validation successful"; + result.value_body_ = std::move(result_value.value()); + std::move(result_callback_).Run(std::move(result)); + } + + void APIRequestHelper::URLLoaderHandler::MaybeSendResult() { + DCHECK_LE(0, current_decoding_operation_count_); + const bool decoding_is_complete = (current_decoding_operation_count_ == 0); + + if (request_is_finished_ && decoding_is_complete) { + std::move(result_callback_).Run(ToAPIRequestResult(std::move(url_loader_))); + } else if (decoding_is_complete) { + VLOG(0) << "Did not run URLLoaderHandler completion handler, still have " + << current_decoding_operation_count_ + << " decoding operations in progress, waiting for them to" + << " complete..."; + } + } + + void APIRequestHelper::URLLoaderHandler::ParseSSE( + std::string_view string_piece) { + // New chunks should only be received before the request is completed + DCHECK(!request_is_finished_); + + // We split the string into multiple chunks because there are cases where + // multiple chunks are received in a single call. + std::vector stream_data = base::SplitStringPiece( + string_piece, "\r\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); + DVLOG(0) << "StringPiece(string_view): " << string_piece; + + // Occasionally, one of the response chunks may be divided into + // two parts—specifically, the last chunk of one response and the + // first chunk of the following response. We need to merge these + // chunks to create valid JSON. The last invalid chunk will be + // discarded, and the merged chunk will replace the first invalid + // chunk of the subsequent response. + static constexpr char kDataPrefix[] = "data: {"; + static constexpr char kDataSuffix[] = "}]}"; + + std::vector stream_data_copy; + + if (!stream_data.empty()) { + bool is_first_piece_invalid = false; + bool is_last_piece_invalid = false; + auto first = stream_data[0] ; + if (!base::StartsWith(first, kDataPrefix)) { + is_first_piece_invalid = true; + DVLOG(0) << "Chunk doesn't start with SSE prefix. Invalid JSON."; + DVLOG(0) << "Invalid first chunk: " << first; + if (!previous_invalid_piece_of_response_chunk_.empty()) { + std::string combined_chunk = std::string(previous_invalid_piece_of_response_chunk_) + (std::string(first)); + stream_data_copy.push_back(combined_chunk); + previous_invalid_piece_of_response_chunk_ = ""; + DVLOG(0) << "Replaced invalid chunk with valid one: " + << combined_chunk; + } + } + if (stream_data.size() > 1) { + auto last = stream_data[stream_data.size() - 1]; + if (base::StartsWith(last, kDataPrefix) && + !base::EndsWith(last, kDataSuffix)) { + is_last_piece_invalid = true; + DVLOG(0) << "Chunk starts with SSE prefix but doesn't end with " + "SSE suffix. Invalid JSON."; + DVLOG(0) << "Invalid last chunk: " << last; + previous_invalid_piece_of_response_chunk_ = std::string(last); + } + } + + auto size = stream_data.size(); + for(size_t i = 0; i < size; i++) { + if (is_first_piece_invalid && i == 0) continue; + if (is_last_piece_invalid && i == size - 1) continue; + stream_data_copy.push_back(std::string(stream_data[i])); + } + } + + // Keep track of number of in-progress data decoding operations + // so that we can know if any are still in-progress when the request + // completes. + current_decoding_operation_count_ += stream_data_copy.size(); + + for (const auto& data : stream_data_copy) { + auto start_index = strlen(kDataPrefix) - 1; + if (start_index >= data.size()) { + DVLOG(0) << "Start index is out of bounds."; + continue; + } + auto json = data.substr(start_index); + auto on_json_parsed = + [](base::WeakPtr handler, + ValueOrError result) { + DVLOG(2) << "Chunk parsed"; + if (!handler) { + return; + } + handler->current_decoding_operation_count_--; + DCHECK(handler->data_received_callback_); + handler->data_received_callback_.Run(std::move(result)); + // Parsing is potentially the last operation for |URLLoaderHandler|. + handler->MaybeSendResult(); + }; + + DVLOG(0) << "Going to call ParseJsonImpl"; + ParseJsonImpl(std::string(json), + base::BindOnce(std::move(on_json_parsed), + weak_ptr_factory_.GetWeakPtr())); + } + } + + void APIRequestHelper::SetUrlLoaderFactoryForTesting( + scoped_refptr url_loader_factory) { + url_loader_factory_ = std::move(url_loader_factory); + } + + void SanitizeAndParseJson(std::string json, + base::OnceCallback callback) { + data_decoder::DataDecoder::ParseJsonIsolated(json, std::move(callback)); + } +} // namespace api_request_helper diff --git a/chrome/browser/ui/webui/side_panel/chat/api/api_request_helper.h b/chrome/browser/ui/webui/side_panel/chat/api/api_request_helper.h new file mode 100644 index 00000000000000..2add7ab8a4b0cc --- /dev/null +++ b/chrome/browser/ui/webui/side_panel/chat/api/api_request_helper.h @@ -0,0 +1,277 @@ +#ifndef CHROMIUM_API_REQUEST_HELPER_H +#define CHROMIUM_API_REQUEST_HELPER_H + +#include +#include +#include +#include +#include + +#include "base/containers/flat_map.h" +#include "base/files/file_path.h" +#include "base/functional/callback.h" +#include "base/functional/callback_forward.h" +#include "base/functional/callback_helpers.h" +#include "base/time/time.h" +#include "base/types/expected.h" +#include "base/values.h" +#include "net/traffic_annotation/network_traffic_annotation.h" +#include "services/network/public/cpp/simple_url_loader.h" +#include "services/network/public/cpp/simple_url_loader_stream_consumer.h" +#include "url/gurl.h" + +namespace network { + class SharedURLLoaderFactory; +} // namespace network + +namespace data_decoder { + class DataDecoder; +} + +namespace api_request_helper { + + class APIRequestResult { + public: + APIRequestResult(); + APIRequestResult(int response_code, + base::Value value_body, + base::flat_map headers, + int error_code, + GURL final_url); + APIRequestResult(const APIRequestResult&) = delete; + APIRequestResult& operator=(const APIRequestResult&) = delete; + APIRequestResult(APIRequestResult&&); + APIRequestResult& operator=(APIRequestResult&&); + ~APIRequestResult(); + + bool operator==(const APIRequestResult& other) const; + bool operator!=(const APIRequestResult& other) const; + + bool Is2XXResponseCode() const; + bool IsResponseCodeValid() const; + + // HTTP response code. + int response_code() const { return response_code_; } + + // Extract the sanitized response as base::Value. + base::Value TakeBody(); + + // Returns the sanitized response as base::Value. + // Note: don't clone large responses, use TakeBody() instead. + const base::Value& value_body() const { return value_body_; } + + // Serialize the sanitized response and returns it as string. + // Note: use TakeBody()/value_body() instead where possible. + std::string SerializeBodyToString() const; + + // HTTP response headers. + const base::flat_map& headers() const { + return headers_; + } + // `net::Error` code + int error_code() const { return error_code_; } + // Actual url requested. May differ from original request url in case of + // redirects happened. + GURL final_url() const { return final_url_; } + + private: + friend class APIRequestHelper; + + int response_code_ = -1; + base::Value value_body_; + base::flat_map headers_; + int error_code_ = -1; + GURL final_url_; + bool body_consumed_ = false; + }; + + struct APIRequestOptions { + bool auto_retry_on_network_change = false; + bool enable_cache = false; + size_t max_body_size = -1u; + std::optional timeout; + }; + + using ValueOrError = base::expected; + + class APIRequestHelper { + public: + using DataReceivedCallback = + base::RepeatingCallback; + using ResultCallback = base::OnceCallback; + using ResponseStartedCallback = + base::OnceCallback; + using ResponseConversionCallback = + base::OnceCallback( + const std::string& raw_response)>; + + class URLLoaderHandler : public network::SimpleURLLoaderStreamConsumer { + public: + URLLoaderHandler(APIRequestHelper* api_request_helper, + scoped_refptr task_runner); + + ~URLLoaderHandler() override; + + URLLoaderHandler(const URLLoaderHandler&) = delete; + + URLLoaderHandler& operator=(const URLLoaderHandler&) = delete; + + void RegisterURLLoader(std::unique_ptr loader); + + void SetResultCallback(ResultCallback result_callback); + + base::WeakPtr GetWeakPtr(); + + private: + friend class APIRequestHelper; + + void ParseJsonImpl(std::string json, + base::OnceCallback callback); + + // Run completion callback if there are no operations in progress. + // If Cancel is needed even if url or data operations are in progress, + // then call |APIRequestHelper::Cancel|. + void MaybeSendResult(); + + void ParseSSE(std::string_view string_piece); + + // network::SimpleURLLoaderStreamConsumer implementation: + void OnDataReceived(std::string_view string_piece, + base::OnceClosure resume) override; + + void OnComplete(bool success) override; + + void OnRetry(base::OnceClosure start_retry) override; + + // This is used for one shot responses + void OnResponse(ResponseConversionCallback conversion_callback, + const std::unique_ptr response_body); + + // Decode one shot responses + void OnParseJsonResponse(APIRequestResult result, + ValueOrError result_value); + + std::unique_ptr url_loader_; + raw_ptr api_request_helper_; + + DataReceivedCallback data_received_callback_; + ResponseStartedCallback response_started_callback_; + ResultCallback result_callback_; + ResponseConversionCallback conversion_callback_; + + bool is_sse_ = false; + + // To ensure ordered processing of stream chunks, we create our own + // instance of DataDecoder per request. This avoids the issue + // of unordered chunks that can occur when calling the static function, + // which creates a new instance of the process for each call. By using a + // single instance of the parser, we can reuse it for consecutive calls. + std::unique_ptr data_decoder_; + // Keep track of number of in-progress data decoding operations + // so that we can know if any are still in-progress when the request + // completes. + int current_decoding_operation_count_ = 0; + bool request_is_finished_ = false; + + std::string previous_invalid_piece_of_response_chunk_; + + const scoped_refptr task_runner_; + + base::WeakPtrFactory weak_ptr_factory_{this}; + }; + + using URLLoaderHandlerList = std::list>; + using Ticket = std::list>::iterator; + + APIRequestHelper( + net::NetworkTrafficAnnotationTag annotation_tag, + scoped_refptr url_loader_factory); + + virtual ~APIRequestHelper(); + + // Each response is expected in json format and will be validated through + // JsonSanitizer. In cases where json contains values that are not supported + // by the standard base/json parser it is necessary to convert such values + // into string before validating the response. For these purposes + // conversion_callback is added which receives raw response and can perform + // necessary conversions. + Ticket Request( + const std::string& method, + const GURL& url, + const std::string& payload, + const std::string& payload_content_type, + ResultCallback callback, + const base::flat_map& headers = {}, + const APIRequestOptions& request_options = {}, + ResponseConversionCallback conversion_callback = base::NullCallback()); + + virtual Ticket RequestSSE( + const std::string& method, + const GURL& url, + const std::string& payload, + const std::string& payload_content_type, + DataReceivedCallback data_received_callback, + ResultCallback result_callback, + const base::flat_map& headers, + const APIRequestOptions& request_options); + + virtual Ticket RequestSSE( + const std::string& method, + const GURL& url, + const std::string& payload, + const std::string& payload_content_type, + DataReceivedCallback data_received_callback, + ResultCallback result_callback, + const base::flat_map& headers, + const APIRequestOptions& request_options, + ResponseStartedCallback response_started_callback); + + void Cancel(const Ticket& ticket); + + void CancelAll(); + + void SetUrlLoaderFactoryForTesting( + scoped_refptr url_loader_factory); + + private: + APIRequestHelper(const APIRequestHelper&) = delete; + + APIRequestHelper& operator=(const APIRequestHelper&) = delete; + + APIRequestHelper::Ticket CreateURLLoaderHandler( + const std::string& method, + const GURL& url, + const std::string& payload, + const std::string& payload_content_type, + bool auto_retry_on_network_change, + bool enable_cache, + bool allow_http_error_result, + const base::flat_map& headers); + + APIRequestHelper::Ticket CreateRequestURLLoaderHandler( + const std::string& method, + const GURL& url, + const std::string& payload, + const std::string& payload_content_type, + const APIRequestOptions& request_options, + const base::flat_map& headers, + ResultCallback result_callback); + + void DeleteAndSendResult(Ticket iter, + ResultCallback callback, + APIRequestResult result); + + net::NetworkTrafficAnnotationTag annotation_tag_; + URLLoaderHandlerList url_loaders_; + scoped_refptr url_loader_factory_; + const scoped_refptr task_runner_; + base::WeakPtrFactory weak_ptr_factory_{this}; + }; + + void SanitizeAndParseJson(std::string json, + base::OnceCallback callback); + +} // namespace api_request_helper + +#endif // CHROMIUM_API_REQUEST_HELPER_H diff --git a/chrome/browser/ui/webui/side_panel/chat/api/completion_api_client.cc b/chrome/browser/ui/webui/side_panel/chat/api/completion_api_client.cc new file mode 100644 index 00000000000000..a0894c841271fd --- /dev/null +++ b/chrome/browser/ui/webui/side_panel/chat/api/completion_api_client.cc @@ -0,0 +1,171 @@ +#include "completion_api_client.h" + +#include +#include +#include + +#include +#include "base/containers/flat_set.h" +#include "base/functional/bind.h" +#include "base/functional/callback_helpers.h" +#include "base/json/json_writer.h" +#include "base/no_destructor.h" +#include "base/strings/strcat.h" +#include "base/values.h" +#include "net/http/http_status_code.h" +#include "net/traffic_annotation/network_traffic_annotation.h" +#include "services/network/public/cpp/simple_url_loader.h" +#include "url/gurl.h" +#include "ui/base/l10n/l10n_util.h" +#include "base/i18n/time_formatting.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "chrome/grit/generated_resources.h" + +namespace { + + constexpr char kHttpMethod[] = "POST"; + + net::NetworkTrafficAnnotationTag GetNetworkTrafficAnnotationTag() { + return net::DefineNetworkTrafficAnnotation("ai_chat", R"( + semantics { + sender: "AI Chat" + description: + "This is used to communicate with Yep Chat api." + trigger: + "Triggered by user sending a prompt." + data: + "Will generate a text that attempts to match the user gave it" + destination: WEBSITE + } + policy { + cookies_allowed: NO + policy_exception_justification: + "Not implemented." + } + )"); + } + + std::string CreateJSONRequestBody(const std::vector& messages) { + base::Value::Dict dict; + dict.Set("stream", true); + dict.Set("max_tokens", 1280); + dict.Set("top_p", 0.7); + dict.Set("temperature", 0.6); + dict.Set("model", "Mixtral-8x7B-Instruct-v0.1"); + + base::Value::List prompt_messages; + for (const auto& item : messages) { + base::Value::Dict message; + message.Set("content", std::move(item.content)); + message.Set("role", item.role); + prompt_messages.Append(std::move(message)); + } + dict.Set("messages", std::move(prompt_messages)); + + std::string json; + base::JSONWriter::Write(dict, &json); + return json; + } +} // namespace + +CompletionApiClient::CompletionApiClient( + scoped_refptr url_loader_factory) + : api_request_helper_(GetNetworkTrafficAnnotationTag(), + std::move(url_loader_factory)) {} + +CompletionApiClient::~CompletionApiClient() = default; + +void CompletionApiClient::QueryPrompt( + const std::vector& completion_messages, + GenerationCompletedCallback data_completed_callback, + GenerationDataCallback + data_received_callback /* = base::NullCallback() */) { + GURL api_url{base::StrCat({url::kHttpsScheme, url::kStandardSchemeSeparator, + "api.yep.com", "/", "v1/chat/completions"})}; + DCHECK(api_url.is_valid()) << "Invalid API Url: " << api_url.spec(); + + base::flat_map headers; + headers.emplace("Accept", "text/event-stream"); + + auto on_received = base::BindRepeating( + &CompletionApiClient::OnQueryDataReceived, weak_ptr_factory_.GetWeakPtr(), + std::move(data_received_callback)); + auto on_complete = base::BindOnce(&CompletionApiClient::OnQueryCompleted, + weak_ptr_factory_.GetWeakPtr(), + std::move(data_completed_callback)); + + const std::string request_body = CreateJSONRequestBody(completion_messages); + + api_request_helper_.RequestSSE(kHttpMethod, api_url, request_body, + "application/json", std::move(on_received), + std::move(on_complete), headers, {}); +} + +void CompletionApiClient::ClearAllQueries() { + DVLOG(0) << "Clearing all queries"; + api_request_helper_.CancelAll(); + entire_completion_result.clear(); +} + +void CompletionApiClient::OnQueryDataReceived( + GenerationDataCallback callback, + base::expected result) { + if (!result.has_value() || !result->is_dict()) { + return; + } + + const base::Value::List* list = result->GetDict().FindList("choices"); + if (list) { + for (const auto& item : *list) { + if (item.is_dict()) { + const base::Value::Dict* delta = item.GetDict().FindDict("delta"); + if (delta) { + const std::string* content = delta->FindString("content"); + if (content) { + entire_completion_result.push_back(*content); + callback.Run(std::move(*content)); + } + } + } + } + } +} + +void CompletionApiClient::OnQueryCompleted( + GenerationCompletedCallback callback, + APIRequestResult result) { + const bool success = result.Is2XXResponseCode(); + + if (success) { + entire_completion_result.clear(); + std::move(callback).Run(base::ok("")); + return; + } + + // Handle error + chat::mojom::APIErrorType error; + DVLOG(0) << "Error response_code: " << result.response_code(); + DVLOG(0) << "Error error_code: " << result.error_code(); + + if (result.value_body().is_dict()) { + const std::string* value = + result.value_body().GetDict().FindString("message"); + if (value) { + // Trimming necessary for Llama 2 which prepends responses with a " ". + auto error_message = base::TrimWhitespaceASCII(*value, base::TRIM_ALL); + DVLOG(0) << "Error message: " << error_message; + } + } + + if (net::HTTP_TOO_MANY_REQUESTS == result.response_code()) { + error = chat::mojom::APIErrorType::RateLimitReached; + } else if (net::HTTP_REQUEST_ENTITY_TOO_LARGE == result.response_code()) { + error = chat::mojom::APIErrorType::ContextLimitReached; + } else { + error = chat::mojom::APIErrorType::ConnectionError; + } + + std::move(callback).Run(base::unexpected(std::move(error))); +} + diff --git a/chrome/browser/ui/webui/side_panel/chat/api/completion_api_client.h b/chrome/browser/ui/webui/side_panel/chat/api/completion_api_client.h new file mode 100644 index 00000000000000..bb601efeee38a5 --- /dev/null +++ b/chrome/browser/ui/webui/side_panel/chat/api/completion_api_client.h @@ -0,0 +1,64 @@ +#ifndef CHROMIUM_COMPLETION_API_CLIENT_H +#define CHROMIUM_COMPLETION_API_CLIENT_H + +#include +#include +#include +#include +#include + +#include "base/containers/flat_set.h" +#include "base/functional/callback_forward.h" +#include "base/memory/weak_ptr.h" +#include "base/types/expected.h" +#include "chrome/browser/ui/webui/side_panel/chat/api/api_request_helper.h" +#include "chrome/browser/ui/webui/side_panel/chat/chat.mojom.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" + +namespace network { + class SharedURLLoaderFactory; +} // namespace network + +using api_request_helper::APIRequestResult; + +struct CompletionMessage { + std::string content; + std::string role; +}; + +class CompletionApiClient { +public: + using GenerationResult = + base::expected; + using GenerationDataCallback = base::RepeatingCallback; + using GenerationCompletedCallback = base::OnceCallback; + + CompletionApiClient( + scoped_refptr url_loader_factory); + + CompletionApiClient(const CompletionApiClient&) = delete; + CompletionApiClient& operator=(const CompletionApiClient&) = delete; + virtual ~CompletionApiClient(); + + // In non-SSE cases, only the data_completed_callback will be triggered. + virtual void QueryPrompt( + const std::vector& completion_messages, + GenerationCompletedCallback data_completed_callback, + GenerationDataCallback data_received_callback = base::NullCallback()); + + // Clears all in-progress requests + void ClearAllQueries(); + +private: + void OnQueryDataReceived(GenerationDataCallback callback, + base::expected result); + void OnQueryCompleted(GenerationCompletedCallback callback, + APIRequestResult result); + + api_request_helper::APIRequestHelper api_request_helper_; + std::vector entire_completion_result; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +#endif // CHROMIUM_COMPLETION_API_CLIENT_H \ No newline at end of file diff --git a/chrome/browser/ui/webui/side_panel/chat/chat.mojom b/chrome/browser/ui/webui/side_panel/chat/chat.mojom new file mode 100644 index 00000000000000..1d71787c896b81 --- /dev/null +++ b/chrome/browser/ui/webui/side_panel/chat/chat.mojom @@ -0,0 +1,103 @@ +module chat.mojom; + +import "url/mojom/url.mojom"; +import "ui/base/mojom/window_open_disposition.mojom"; + +enum APIErrorType { + None, + ConnectionError, + RateLimitReached, + ContextLimitReached, +}; + +enum ResponseType { + NONE, + DELTA, + COMPLETED, + ERROR, +}; + +enum ActionType { + SUMMARIZE_PAGE, + EXPLAIN, + TRANSLATE, + DRAFT_SOCIAL_MEDIA_POST, + FACT_CHECK, + QUERY, + NONE, +}; + +struct APIError { + APIErrorType error_type; + string error_message; +}; + +struct ConversationItem { + string user_query; + string llm_response; +}; + +struct SiteInfo { + // The title of the currently active tab, if it has an open page + string? title; + + // The url of the currently active tab if it has opening page + string? url; + + // Indicates whether the current URL contains content that can be used in conversations with Yep Chat + bool is_content_usable_in_conversations; +}; + +struct ActionItem { + ActionType action_type; + string label; +}; + +struct ActionResponse { + ActionType action_type; + ResponseType response_type; + string result; +}; + +// Used by the WebUI page to bootstrap bidirectional communication. +interface PageHandlerFactory { + CreatePageHandler(pending_remote page, + pending_receiver handler); +}; + +// Browser-side handler for requests from WebUI page. +// Renderer -> Browser +interface PageHandler { + + GetSiteInfo() => (SiteInfo site_info); + + // Show as menu items in Chat UI + // This will be called only when SiteInfo.is_content_usable_in_conversations is true + GetActionList() => (array action_list); + + // The user selects a predefined action by clicking on menu items in the Chat UI + // The content of the current opening page will serve as context + // and a predefined prompt will be applied accordingly. + SubmitAction(ActionType action_type, string param); + + // Usual conversation + SubmitQuery(ActionType action_type, string query, string url, array conversation_history); + + // Notify the backend that the UI is ready to be shown. + ShowUI(); + + // Notify the backend that the dialog should be closed. + CloseUI(); + + CancelQuery(); + + OpenURL(string url, ui.mojom.ClickModifiers click_modifiers); +}; + +// WebUI-side handler for requests from the browser. +// Browser -> Renderer +interface Page { + OnSiteInfoChanged(SiteInfo info); + + OnSubmitActionResponse(ActionResponse response); +}; \ No newline at end of file diff --git a/chrome/browser/ui/webui/side_panel/chat/chat_page_handler.cc b/chrome/browser/ui/webui/side_panel/chat/chat_page_handler.cc new file mode 100644 index 00000000000000..25771ee7ea2710 --- /dev/null +++ b/chrome/browser/ui/webui/side_panel/chat/chat_page_handler.cc @@ -0,0 +1,349 @@ +#include "chat_page_handler.h" + +#include +#include +#include +#include + +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "chrome/app/chrome_command_ids.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_commands.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_window/public/browser_window_features.h" +#include "chrome/browser/ui/views/side_panel/side_panel_ui.h" +#include "chrome/grit/generated_resources.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/browser/web_contents.h" +#include "services/service_manager/public/cpp/interface_provider.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/base/mojom/window_open_disposition.mojom.h" +#include "ui/base/window_open_disposition.h" +#include "ui/base/window_open_disposition_utils.h" + +namespace { + + constexpr size_t + kMaxUserPromptLength = 90'000; + constexpr char kUserRole[] = "user"; + constexpr char kAssistantRole[] = "assistant"; + + std::string BuildPrompt(const std::string &query, + const std::string &extracted_content, + chat::mojom::ActionType action_type) { + if (action_type == chat::mojom::ActionType::SUMMARIZE_PAGE || + action_type == chat::mojom::ActionType::EXPLAIN || + action_type == chat::mojom::ActionType::FACT_CHECK || + action_type == chat::mojom::ActionType::TRANSLATE || + action_type == chat::mojom::ActionType::DRAFT_SOCIAL_MEDIA_POST) { + return query + ": " + extracted_content; + } else { + std::string context_prompt = + l10n_util::GetStringUTF8(IDS_CHAT_CONTEXT_PROMPT); + return base::ReplaceStringPlaceholders(context_prompt, + {query, extracted_content}, nullptr); + } + } +} // namespace + +ChatPageHandler::ChatPageHandler( + mojo::PendingReceiver receiver, + mojo::PendingRemote page, + ChatUI *chat_ui, + content::WebUI *web_ui, + content::WebContents *owner_web_contents, + content::WebContents *chat_context_web_contents) + : receiver_(this, std::move(receiver)), + page_(std::move(page)), + chat_ui_(chat_ui), + owner_web_contents_(owner_web_contents->GetWeakPtr()), + chat_context_web_contents_(chat_context_web_contents->GetWeakPtr()), + profile_(Profile::FromWebUI(web_ui)), + page_content_extractor_helper_(std::make_unique(chat_context_web_contents)) { + isQueryCancellingInProgress_.store(false); + scoped_refptr url_loader_factory = + profile_->GetDefaultStoragePartition() + ->GetURLLoaderFactoryForBrowserProcess(); + api_client_ = + std::make_unique(std::move(url_loader_factory)); +} + +ChatPageHandler::~ChatPageHandler() = default; + +void ChatPageHandler::ShowUI() { + auto embedder = chat_ui_->embedder(); + if (embedder) { + embedder->ShowUI(); + } +} + +void ChatPageHandler::CloseUI() { + auto embedder = chat_ui_->embedder(); + if (embedder) { + embedder->CloseUI(); + } + + Browser *browser = chrome::FindLastActive(); + if (!browser) { + return; + } + + if (SidePanelUI *ui = browser->GetFeatures().side_panel_ui()) { + ui->Close(); + } +} + +void ChatPageHandler::SetSiteInfo(chat::mojom::SiteInfoPtr site_info, content::WebContents *contents) { + page_content_extractor_helper_ = std::make_unique(contents); + chat_context_web_contents_ = contents->GetWeakPtr(); + if (page_.is_bound()) { + page_->OnSiteInfoChanged(std::move(site_info)); + } +} + +void ChatPageHandler::GetSiteInfo(GetSiteInfoCallback callback) { + DCHECK(chat_context_web_contents_); + chat::mojom::SiteInfoPtr site_info = chat::mojom::SiteInfo::New(); + site_info->url = ""; + site_info->is_content_usable_in_conversations = false; + + if (chat_context_web_contents_) { + const GURL gurl = chat_context_web_contents_->GetLastCommittedURL(); + if (gurl.SchemeIsHTTPOrHTTPS()) { + site_info->title = + base::UTF16ToUTF8(chat_context_web_contents_->GetTitle()); + site_info->url = gurl.spec(); + site_info->is_content_usable_in_conversations = true; + } else { + site_info->title = ""; + site_info->url = ""; + site_info->is_content_usable_in_conversations = false; + } + } + + std::move(callback).Run(site_info.Clone()); +} + +void ChatPageHandler::GetActionList(GetActionListCallback callback) { + std::vector action_items; + + chat::mojom::ActionItemPtr summarize_item = chat::mojom::ActionItem::New( + chat::mojom::ActionType::SUMMARIZE_PAGE, + l10n_util::GetStringUTF8(IDS_CHAT_SUMMARIZE_THIS_PAGE)); + + chat::mojom::ActionItemPtr explain_item = chat::mojom::ActionItem::New( + chat::mojom::ActionType::EXPLAIN, + l10n_util::GetStringUTF8(IDS_CHAT_EXPLAIN_IT_IN_SIMPLE_LANGUAGE)); + + chat::mojom::ActionItemPtr translate_item = chat::mojom::ActionItem::New( + chat::mojom::ActionType::TRANSLATE, + l10n_util::GetStringUTF8(IDS_CHAT_TRANSLATE)); + + chat::mojom::ActionItemPtr draft_social_media_post_item = + chat::mojom::ActionItem::New( + chat::mojom::ActionType::DRAFT_SOCIAL_MEDIA_POST, + l10n_util::GetStringUTF8(IDS_CHAT_DRAFT_A_SOCIAL_MEDIA_POST)); + + chat::mojom::ActionItemPtr fact_check_item = chat::mojom::ActionItem::New( + chat::mojom::ActionType::FACT_CHECK, + l10n_util::GetStringUTF8(IDS_CHAT_DRAFT_FACT_CHECK)); + + action_items.push_back(summarize_item.Clone()); + action_items.push_back(explain_item.Clone()); + action_items.push_back(translate_item.Clone()); + action_items.push_back(draft_social_media_post_item.Clone()); + action_items.push_back(fact_check_item.Clone()); + + std::move(callback).Run(std::move(action_items)); +} + +base::WeakPtr ChatPageHandler::GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); +} + +void ChatPageHandler::SubmitAction(chat::mojom::ActionType action_type, + const std::string &action_param) { + isQueryCancellingInProgress_.store(false); + if (page_.is_bound()) { + std::string summarize_prompt = + l10n_util::GetStringUTF8(IDS_CHAT_PROMPT_SUMMARIZE_THIS_PAGE); + std::string explain_prompt = l10n_util::GetStringUTF8( + IDS_CHAT_PROMPT_EXPLAIN_IT_IN_SIMPLE_LANGUAGE); + std::string fact_check_prompt = + l10n_util::GetStringUTF8(IDS_CHAT_PROMPT_DRAFT_FACT_CHECK); + std::string translate_to_prompt = + l10n_util::GetStringUTF8(IDS_CHAT_PROMPT_TRANSLATE); + std::string draft_social_media_post_prompt = + l10n_util::GetStringUTF8(IDS_CHAT_PROMPT_DRAFT_A_SOCIAL_MEDIA_POST); + + std::vector completion_messages = {}; + + if (action_type == chat::mojom::ActionType::SUMMARIZE_PAGE) { + page_content_extractor_helper_->ExtractPageContent(base::BindOnce( + &ChatPageHandler::OnPageContentExtracted, base::Unretained(this), + action_type, summarize_prompt, completion_messages)); + + } else if (action_type == chat::mojom::ActionType::EXPLAIN) { + page_content_extractor_helper_->ExtractPageContent( + base::BindOnce(&ChatPageHandler::OnPageContentExtracted, + base::Unretained(this), action_type, + explain_prompt, completion_messages)); + + } else if (action_type == chat::mojom::ActionType::FACT_CHECK) { + page_content_extractor_helper_->ExtractPageContent( + base::BindOnce(&ChatPageHandler::OnPageContentExtracted, + base::Unretained(this), action_type, + fact_check_prompt, completion_messages)); + } else if (action_type == chat::mojom::ActionType::TRANSLATE) { + page_content_extractor_helper_->ExtractPageContent(base::BindOnce( + &ChatPageHandler::OnPageContentExtracted, + base::Unretained(this), action_type, + translate_to_prompt + " " + action_param, completion_messages)); + } else if (action_type == + chat::mojom::ActionType::DRAFT_SOCIAL_MEDIA_POST) { + page_content_extractor_helper_->ExtractPageContent(base::BindOnce( + &ChatPageHandler::OnPageContentExtracted, + base::Unretained(this), action_type, + draft_social_media_post_prompt + " " + action_param, + completion_messages)); + } else { + // this branch should not be reached because all the action items are + // handled in above blocks + } + } +} + +void ChatPageHandler::OnPageContentExtracted( + chat::mojom::ActionType action_type, + const std::string &prompt, + const std::vector &completion_messages, + std::string content, + std::string url) { + + extracted_content_cache_.clear(); + + std::string max_content = content; + if (content.length() > kMaxUserPromptLength) { + max_content = content.substr(0, kMaxUserPromptLength); + } + + if (!url.empty()) { + extracted_content_cache_[url] = max_content; + } + + if (isQueryCancellingInProgress_.load()) { + isQueryCancellingInProgress_.store(false); + return; + } + + std::vector all_messages; + for (auto &msg: completion_messages) { + all_messages.push_back(msg); + } + all_messages.push_back( + {BuildPrompt(prompt, max_content, action_type), kUserRole}); + + api_client_->QueryPrompt( + all_messages, + base::BindOnce(&ChatPageHandler::SubmitQueryCompletedCallback, + base::Unretained(this), action_type), + base::BindRepeating(&ChatPageHandler::SubmitQueryCallback, + base::Unretained(this), action_type)); +} + +void ChatPageHandler::SubmitQuery(chat::mojom::ActionType action_type, + const std::string &query, + const std::string &url, + std::vector conversation_history) { + + std::vector completion_messages; + + for (auto &item: conversation_history) { + completion_messages.push_back({item->user_query, kUserRole}); + completion_messages.push_back({item->llm_response, kAssistantRole}); + } + + if (extracted_content_cache_.contains(url) /* Context is in the cache */) { + auto previous_content = extracted_content_cache_[url]; + completion_messages.push_back( + {BuildPrompt(query, previous_content, action_type), kUserRole}); + api_client_->QueryPrompt( + completion_messages, + base::BindOnce(&ChatPageHandler::SubmitQueryCompletedCallback, + base::Unretained(this), action_type), + base::BindRepeating(&ChatPageHandler::SubmitQueryCallback, + base::Unretained(this), action_type)); + + } else if (!url.empty()/* Context is not in the cache; user visit new page so new content should be extracted */) { + page_content_extractor_helper_->ExtractPageContent(base::BindOnce( + &ChatPageHandler::OnPageContentExtracted, base::Unretained(this), + action_type, query, completion_messages)); + } else /* user removed the context via Chat UI or the current opening tab is + empty */ + { + completion_messages.push_back({query, kUserRole}); + api_client_->QueryPrompt( + completion_messages, + base::BindOnce(&ChatPageHandler::SubmitQueryCompletedCallback, + base::Unretained(this), action_type), + base::BindRepeating(&ChatPageHandler::SubmitQueryCallback, + base::Unretained(this), action_type)); + } +} + +void ChatPageHandler::SubmitQueryCallback(chat::mojom::ActionType action_type, + std::string completion) { + chat::mojom::ActionResponsePtr response = chat::mojom::ActionResponse::New(); + response->action_type = action_type; + response->response_type = chat::mojom::ResponseType::DELTA; + response->result = std::move(completion); + page_->OnSubmitActionResponse(response.Clone()); +} + +void ChatPageHandler::SubmitQueryCompletedCallback( + chat::mojom::ActionType action_type, + base::expected result) { + chat::mojom::ActionResponsePtr response = chat::mojom::ActionResponse::New(); + response->action_type = action_type; + + if (result.has_value()) { + DVLOG(0) << __func__ << " success -> " << result.value(); + response->response_type = chat::mojom::ResponseType::COMPLETED; + response->result = result.value(); + } else { + DVLOG(0) << __func__ << " error -> " << result.error(); + response->response_type = chat::mojom::ResponseType::ERROR; + response->result = l10n_util::GetStringUTF8(IDS_CHAT_GENERIC_ERROR); + } + page_->OnSubmitActionResponse(response.Clone()); +} + +void ChatPageHandler::CancelQuery() { + isQueryCancellingInProgress_.store(true); + api_client_->ClearAllQueries(); +} + +void ChatPageHandler::OpenURL( + const std::string &url, + ui::mojom::ClickModifiersPtr click_modifiers) { + Browser *browser = chrome::FindLastActive(); + if (!browser) { + return; + } + + // Open in active tab if the user is on the NTP. + WindowOpenDisposition open_location = ui::DispositionFromClick( + click_modifiers->middle_button, click_modifiers->alt_key, + click_modifiers->ctrl_key, click_modifiers->meta_key, + click_modifiers->shift_key); + + GURL gurl(url); + + if (gurl.is_valid()) { + content::OpenURLParams params(gurl, content::Referrer(), open_location, + ui::PAGE_TRANSITION_AUTO_BOOKMARK, false); + browser->OpenURL(params, /*navigation_handle_callback=*/{}); + } +} \ No newline at end of file diff --git a/chrome/browser/ui/webui/side_panel/chat/chat_page_handler.h b/chrome/browser/ui/webui/side_panel/chat/chat_page_handler.h new file mode 100644 index 00000000000000..8d1f6a4796a17a --- /dev/null +++ b/chrome/browser/ui/webui/side_panel/chat/chat_page_handler.h @@ -0,0 +1,102 @@ +#ifndef CHROMIUM_CHAT_PAGE_HANDLER_H +#define CHROMIUM_CHAT_PAGE_HANDLER_H + +#include +#include +#include +#include + +#include "base/containers/flat_set.h" +#include "base/functional/callback_forward.h" +#include "base/memory/raw_ptr.h" +#include "base/scoped_observation.h" +#include "base/types/expected.h" +#include "chat_ui.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/webui/side_panel/chat/api/completion_api_client.h" +#include "chrome/browser/ui/webui/side_panel/chat/chat.mojom.h" +#include "chrome/common/chat/page_content_extractor.mojom.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_contents_user_data.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "page_content_extractor_helper.h" + +namespace content { + class WebContents; + + class WebUI; +} // namespace content + +class ChatPageHandler : public chat::mojom::PageHandler { + public: + ChatPageHandler(mojo::PendingReceiver receiver, + mojo::PendingRemote page, + ChatUI* chat_ui, + content::WebUI* web_ui, + content::WebContents* owner_web_contents, + content::WebContents* chat_context_web_contents); + + ChatPageHandler(const ChatPageHandler&) = delete; + + ChatPageHandler& operator=(const ChatPageHandler&) = delete; + + ~ChatPageHandler() override; + + void GetSiteInfo(GetSiteInfoCallback callback) override; + + void GetActionList(GetActionListCallback callback) override; + + void SubmitAction(chat::mojom::ActionType action_type, + const std::string& action_param) override; + + void SubmitQuery(chat::mojom::ActionType action_type, + const std::string& query, + const std::string& url, + std::vector conversation_history) override; + + void ShowUI() override; + + void CloseUI() override; + + void SetSiteInfo(chat::mojom::SiteInfoPtr site_info, + content::WebContents* contents); + + void SubmitQueryCallback(chat::mojom::ActionType action_type, + std::string completion); + + void SubmitQueryCompletedCallback( + chat::mojom::ActionType action_type, + base::expected result); + + void CancelQuery() override; + + void OpenURL(const std::string& url, ui::mojom::ClickModifiersPtr click_modifiers) override; + + base::WeakPtr GetWeakPtr(); + + private: + void OnPageContentExtracted( + chat::mojom::ActionType action_type, + const std::string& prompt, + const std::vector& completion_messages, + std::string content, + std::string url); + + mojo::Receiver receiver_; + mojo::Remote page_; + const raw_ptr chat_ui_ = nullptr; + base::WeakPtr owner_web_contents_ = nullptr; + base::WeakPtr chat_context_web_contents_ = nullptr; + const raw_ptr profile_ = nullptr; + std::unique_ptr api_client_ = nullptr; + std::unique_ptr page_content_extractor_helper_ = + nullptr; + std::atomic isQueryCancellingInProgress_; + base::flat_map extracted_content_cache_; + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +#endif //CHROMIUM_CHAT_PAGE_HANDLER_H diff --git a/chrome/browser/ui/webui/side_panel/chat/chat_ui.cc b/chrome/browser/ui/webui/side_panel/chat/chat_ui.cc new file mode 100644 index 00000000000000..0a7d9577caaa66 --- /dev/null +++ b/chrome/browser/ui/webui/side_panel/chat/chat_ui.cc @@ -0,0 +1,158 @@ + +#ifdef UNSAFE_BUFFERS_BUILD +#pragma allow_unsafe_buffers +#endif + +#include "chat_ui.h" + +#include "chat_page_handler.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_window.h" +#include "chrome/browser/ui/browser_window/public/browser_window_features.h" +#include "chrome/browser/ui/views/frame/browser_view.h" +#include "chrome/browser/ui/views/side_panel/side_panel_ui.h" +#include "chrome/browser/ui/webui/webui_util.h" +#include "chrome/common/webui_url_constants.h" +#include "chrome/grit/generated_resources.h" +#include "chrome/grit/side_panel_chat_resources.h" +#include "chrome/grit/side_panel_chat_resources_map.h" +#include "chrome/grit/side_panel_shared_resources.h" +#include "chrome/grit/side_panel_shared_resources_map.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_ui.h" +#include "content/public/browser/web_ui_controller.h" +#include "content/public/browser/web_ui_data_source.h" +#include "content/public/browser/webui_config.h" +#include "content/public/common/url_constants.h" +#include "ui/base/ui_base_features.h" +#include "ui/base/webui/web_ui_util.h" +#include "ui/views/style/platform_style.h" +#include "ui/webui/color_change_listener/color_change_handler.h" +#include "ui/webui/mojo_web_ui_controller.h" + +namespace { + +#if BUILDFLAG(IS_ANDROID) +content::WebContents* GetActiveWebContents(content::BrowserContext* context) { + auto tab_models = TabModelList::models(); + auto iter = base::ranges::find_if( + tab_models, [](const auto& model) { return model->IsActiveModel(); }); + if (iter == tab_models.end()) { + return nullptr; + } + + auto* active_contents = (*iter)->GetActiveWebContents(); + if (!active_contents) { + return nullptr; + } + DCHECK_EQ(active_contents->GetBrowserContext(), context); + return active_contents; +} +#endif + +Browser* GetBrowserForWebContents(content::WebContents* web_contents) { + if (!web_contents) { + return nullptr; + } + + auto* browser_window = + BrowserWindow::FindBrowserWindowWithWebContents(web_contents); + auto* browser_view = static_cast(browser_window); + if (!browser_view) { + return nullptr; + } + + return browser_view->browser(); +} + +} // namespace + +ChatUI::ChatUI(content::WebUI* web_ui) + : TopChromeWebUIController(web_ui) { + Profile* const profile = Profile::FromWebUI(web_ui); + content::WebUIDataSource* source = content::WebUIDataSource::CreateAndAdd( + profile, chrome::kChromeUIChatHost); + + static constexpr webui::LocalizedString kLocalizedStrings[] = { + {"title", IDS_AI_CHAT_TITLE}, + {"askAnything", IDS_CHAT_ASK_ANYTHING}, + {"chatAboutThisPage", IDS_CHAT_CHAT_ABOUT_THIS_PAGE}, + {"promptSummarizeThisPage", IDS_CHAT_PROMPT_SUMMARIZE_THIS_PAGE}, + {"promptExplainInSimpleLanguage", + IDS_CHAT_PROMPT_EXPLAIN_IT_IN_SIMPLE_LANGUAGE}, + {"promptFactCheck", IDS_CHAT_PROMPT_DRAFT_FACT_CHECK}, + {"promptTranslate", IDS_CHAT_PROMPT_TRANSLATE}, + {"promptSocialMediaPost", IDS_CHAT_PROMPT_DRAFT_A_SOCIAL_MEDIA_POST}, + {"translateLanguages", IDS_CHAT_TRANSLATE_LANGS}, + {"socialMedias", IDS_CHAT_SOCIAL_MEDIAS}, + {"promptExceedMaxTokenCount", IDS_CHAT_PROMPT_INPUT_EXCEED}, + {"genericError", IDS_CHAT_GENERIC_ERROR}, + {"closeSidePanel", IDS_CHAT_SIDE_PANEL_CLOSE}, + {"newChat", IDS_CHAT_NEW_CHAT}, + }; + + for (const auto& str : kLocalizedStrings) + webui::AddLocalizedString(source, str.name, str.id); + + webui::SetupWebUIDataSource( + source, + base::make_span(kSidePanelChatResources, kSidePanelChatResourcesSize), + IDR_SIDE_PANEL_CHAT_CHAT_HTML); +} + +ChatUI::~ChatUI() = default; + +WEB_UI_CONTROLLER_TYPE_IMPL(ChatUI) + +void ChatUI::BindInterface( + mojo::PendingReceiver + pending_receiver) { + color_provider_handler_ = std::make_unique( + web_ui()->GetWebContents(), std::move(pending_receiver)); +} + +void ChatUI::BindInterface( + mojo::PendingReceiver receiver) { + page_factory_receiver_.reset(); + page_factory_receiver_.Bind(std::move(receiver)); +} + +void ChatUI::CreatePageHandler( + mojo::PendingRemote page, + mojo::PendingReceiver receiver) { + DCHECK(page); + // ShowUI() is called before creating the PageHandler. + // This ensures the WebContents is added to a Browser, + // allowing us to provide the Browser reference to the PageHandler. + if (embedder_) { + embedder_->ShowUI(); + } + + content::WebContents* web_contents = nullptr; +#if !BUILDFLAG(IS_ANDROID) + Browser* browser = GetBrowserForWebContents(web_ui()->GetWebContents()); + if (!browser) { + return; + } + + TabStripModel* tab_strip_model = browser->tab_strip_model(); + DCHECK(tab_strip_model); + web_contents = tab_strip_model->GetActiveWebContents(); +#else + web_contents = GetActiveWebContents(profile_); +#endif + if (web_contents == web_ui()->GetWebContents()) { + web_contents = nullptr; + } + page_handler_ = std::make_unique( + std::move(receiver), std::move(page), this, web_ui(), + web_ui()->GetWebContents(), web_contents); +} + +void ChatUI::SetSiteInfo(chat::mojom::SiteInfoPtr site_info, content::WebContents* contents) { + if (page_handler_) { + page_handler_->SetSiteInfo(std::move(site_info), contents); + } +} diff --git a/chrome/browser/ui/webui/side_panel/chat/chat_ui.h b/chrome/browser/ui/webui/side_panel/chat/chat_ui.h new file mode 100644 index 00000000000000..08a4a4caec6472 --- /dev/null +++ b/chrome/browser/ui/webui/side_panel/chat/chat_ui.h @@ -0,0 +1,76 @@ +#ifndef CHROMIUM_CHAT_UI_H +#define CHROMIUM_CHAT_UI_H + +#include "chrome/browser/ui/webui/side_panel/chat/chat.mojom.h" +#include "chrome/browser/ui/webui/top_chrome/top_chrome_web_ui_controller.h" +#include "chrome/browser/ui/webui/top_chrome/top_chrome_webui_config.h" +#include "chrome/common/webui_url_constants.h" +#include "content/public/browser/web_ui_controller.h" +#include "content/public/browser/webui_config.h" +#include "content/public/common/url_constants.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "ui/webui/mojo_web_ui_controller.h" +#include "ui/webui/resources/cr_components/color_change_listener/color_change_listener.mojom.h" + +class ChatPageHandler; + +namespace ui { +class ColorChangeHandler; +} + +class ChatUI; + +class ChatUIConfig : public DefaultTopChromeWebUIConfig { +public: + ChatUIConfig() + : DefaultTopChromeWebUIConfig(content::kChromeUIScheme, + chrome::kChromeUIChatHost) {} +}; + +class ChatUI : public TopChromeWebUIController, + public chat::mojom::PageHandlerFactory { +public: + explicit ChatUI(content::WebUI *web_ui); + + ChatUI(const ChatUI &) = delete; + + ChatUI &operator=(const ChatUI &) = delete; + + ~ChatUI() override; + + void BindInterface( + mojo::PendingReceiver + pending_receiver); + + void BindInterface( + mojo::PendingReceiver receiver); + + // Set by WebUIContentsWrapperT.TopChromeWebUIController provides default + // implementation for this but we don't use it. + void set_embedder( + base::WeakPtr embedder) { + embedder_ = embedder; + } + + static constexpr std::string GetWebUIName() { return "Chat"; } + + void SetSiteInfo(chat::mojom::SiteInfoPtr site_info, content::WebContents* contents ); + + private: + void CreatePageHandler( + mojo::PendingRemote page, + mojo::PendingReceiver receiver) override; + + std::unique_ptr color_provider_handler_; + std::unique_ptr page_handler_; + + mojo::Receiver page_factory_receiver_{ + this}; + + base::WeakPtr embedder_; + + WEB_UI_CONTROLLER_TYPE_DECL(); +}; +#endif //CHROMIUM_CHAT_UI_H diff --git a/chrome/browser/ui/webui/side_panel/chat/page_content_extractor_helper.cc b/chrome/browser/ui/webui/side_panel/chat/page_content_extractor_helper.cc new file mode 100644 index 00000000000000..e7435e0d4dc2a7 --- /dev/null +++ b/chrome/browser/ui/webui/side_panel/chat/page_content_extractor_helper.cc @@ -0,0 +1,121 @@ +#include "page_content_extractor_helper.h" + +#include +#include +#include +#include +#include + +#include "base/functional/callback_forward.h" +#include "base/memory/raw_ptr.h" +#include "base/containers/fixed_flat_set.h" +#include "base/containers/contains.h" +#include "base/functional/bind.h" +#include "base/memory/weak_ptr.h" +#include "base/ranges/algorithm.h" +#include "base/strings/string_util.h" +#include "chrome/common/chat/page_content_extractor.mojom.h" +#include "content/public/browser/navigation_handle.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_contents_user_data.h" +#include "mojo/public/cpp/bindings/associated_receiver.h" +#include "mojo/public/cpp/bindings/pending_associated_receiver.h" +#include "services/service_manager/public/cpp/interface_provider.h" +#include "content/public/browser/web_contents.h" + +namespace { + +class PageContentExtractorInternal { + public: + PageContentExtractorInternal() {} + + void Start( + mojo::Remote content_extractor, + base::OnceCallback callback) { + content_extractor_ = std::move(content_extractor); + if (!content_extractor_) { + DeleteSelf(); + return; + } + + // Ref: + // https://chromium.googlesource.com/chromium/src/+/refs/heads/main/mojo/public/cpp/bindings/README.md#a-note-about-endpoint-lifetime-and-callbacks + // Once a `mojo::Remote` is destroyed, it is guaranteed that pending + // callbacks as well as the connection error handler (if registered) won't + // be called. Once a `mojo::Receiver` is destroyed, it is guaranteed + // that no more method calls are dispatched to the implementation and the + // connection error handler (if registered) won't be called. + content_extractor_.set_disconnect_handler(base::BindOnce( + &PageContentExtractorInternal::DeleteSelf, base::Unretained(this))); + content_extractor_->ExtractPageContent( + base::BindOnce(&PageContentExtractorInternal::OnPageContentExtracted, + base::Unretained(this), std::move(callback))); + } + + void OnPageContentExtracted( + base::OnceCallback callback, + const std::optional& content, + const std::optional& url) { + if (!content.has_value()) { + DVLOG(0) << __func__ << "Extracted content is null."; + SendResultAndDeleteSelf(std::move(callback)); + return; + } + + if (content->empty()) { + DVLOG(0) << __func__ << "Extracted content is empty."; + SendResultAndDeleteSelf(std::move(callback)); + return; + } + + if (!url.has_value()) { + DVLOG(0) << __func__ << "url to extract content is null."; + SendResultAndDeleteSelf(std::move(callback)); + return; + } + + if (url->empty()) { + DVLOG(0) << __func__ << "url to extract content is empty."; + SendResultAndDeleteSelf(std::move(callback)); + return; + } + + DVLOG(0) << __func__ << "extracted page content: " << content.value(); + DVLOG(0) << __func__ << "url of the extracted page content: " << url.value(); + SendResultAndDeleteSelf(std::move(callback), content.value(), url.value()); + } + + private: + void DeleteSelf() { delete this; } + + void SendResultAndDeleteSelf(base::OnceCallback callback, + std::string content = "", std::string url = "") { + std::move(callback).Run(content, url); + delete this; + } + + mojo::Remote content_extractor_; + base::WeakPtrFactory weak_ptr_factory_{this}; +}; +} // namespace + +PageContentExtractorHelper::PageContentExtractorHelper( + content::WebContents* web_contents) + : web_contents_(web_contents->GetWeakPtr()) {} + +PageContentExtractorHelper::~PageContentExtractorHelper() = default; + +void PageContentExtractorHelper::ExtractPageContent( + base::OnceCallback callback) { + auto* primary_rfh = web_contents_->GetPrimaryMainFrame(); + DCHECK(primary_rfh->IsRenderFrameLive()); + + mojo::Remote extractor; + primary_rfh->GetRemoteInterfaces()->GetInterface( + extractor.BindNewPipeAndPassReceiver()); + + auto* internal_extractor = new PageContentExtractorInternal(); + internal_extractor->Start(std::move(extractor), std::move(callback)); +} diff --git a/chrome/browser/ui/webui/side_panel/chat/page_content_extractor_helper.h b/chrome/browser/ui/webui/side_panel/chat/page_content_extractor_helper.h new file mode 100644 index 00000000000000..ffbcdead52991d --- /dev/null +++ b/chrome/browser/ui/webui/side_panel/chat/page_content_extractor_helper.h @@ -0,0 +1,28 @@ +#ifndef CHROMIUM_PAGE_CONTENT_EXTRACTOR_HELPER_H +#define CHROMIUM_PAGE_CONTENT_EXTRACTOR_HELPER_H + +#include + +#include "base/functional/callback_forward.h" +#include "chrome/common/chat/page_content_extractor.mojom.h" +#include "chrome/renderer/chat/page_content_extractor.h" // nogncheck + +namespace content { + class WebContents; +} // namespace content + +class PageContentExtractorHelper{ + public: + explicit PageContentExtractorHelper(content::WebContents* web_contents); + ~PageContentExtractorHelper(); + PageContentExtractorHelper(const PageContentExtractorHelper&) = delete; + PageContentExtractorHelper& operator=(const PageContentExtractorHelper&) = + delete; + void ExtractPageContent( + base::OnceCallback callback); + + private: + base::WeakPtr web_contents_; +}; + +#endif // CHROMIUM_PAGE_CONTENT_EXTRACTOR_HELPER_H diff --git a/chrome/chrome_paks.gni b/chrome/chrome_paks.gni index 4f99bf3e0ee600..03ac57f6ea0aad 100644 --- a/chrome/chrome_paks.gni +++ b/chrome/chrome_paks.gni @@ -193,6 +193,7 @@ template("chrome_extra_paks") { "$root_gen_dir/chrome/side_panel_history_clusters_resources.pak", "$root_gen_dir/chrome/side_panel_read_anything_resources.pak", "$root_gen_dir/chrome/side_panel_reading_list_resources.pak", + "$root_gen_dir/chrome/side_panel_chat_resources.pak", "$root_gen_dir/chrome/side_panel_shared_resources.pak", "$root_gen_dir/chrome/signin_resources.pak", "$root_gen_dir/chrome/suggest_internals_resources.pak", diff --git a/chrome/common/chat/BUILD.gn b/chrome/common/chat/BUILD.gn new file mode 100644 index 00000000000000..3fd8db43976b3f --- /dev/null +++ b/chrome/common/chat/BUILD.gn @@ -0,0 +1,5 @@ +import("//mojo/public/tools/bindings/mojom.gni") + +mojom("mojo_bindings") { + sources = [ "page_content_extractor.mojom" ] +} diff --git a/chrome/common/chat/page_content_extractor.mojom b/chrome/common/chat/page_content_extractor.mojom new file mode 100644 index 00000000000000..74b417d712cb1d --- /dev/null +++ b/chrome/common/chat/page_content_extractor.mojom @@ -0,0 +1,5 @@ +module chat.mojom; + +interface PageContentExtractor { + ExtractPageContent() => (string? page_content, string? url); +}; diff --git a/chrome/common/chrome_isolated_world_ids.h b/chrome/common/chrome_isolated_world_ids.h index 20c1984064b2e7..e789ed8b2dff1f 100644 --- a/chrome/common/chrome_isolated_world_ids.h +++ b/chrome/common/chrome_isolated_world_ids.h @@ -16,6 +16,7 @@ enum ChromeIsolatedWorldIDs { // Isolated world ID for internal Chrome features. ISOLATED_WORLD_ID_CHROME_INTERNAL, + #if BUILDFLAG(IS_MAC) // Isolated world ID for AppleScript. ISOLATED_WORLD_ID_APPLESCRIPT, diff --git a/chrome/common/webui_url_constants.h b/chrome/common/webui_url_constants.h index 9fa26adc16d4fa..b143e07c35cde2 100644 --- a/chrome/common/webui_url_constants.h +++ b/chrome/common/webui_url_constants.h @@ -713,6 +713,8 @@ inline constexpr char kSiteDetailsSubpage[] = "content/siteDetails"; inline constexpr char kSyncSetupSubPage[] = "syncSetup"; inline constexpr char kTriggeredResetProfileSettingsSubPage[] = "triggeredResetProfileSettings"; +inline constexpr char kChromeUIChatURL[] = "chrome://chat/"; +inline constexpr char kChromeUIChatHost[] = "chat"; #if BUILDFLAG(IS_WIN) // TODO(crbug.com/40647483): Remove when issue is resolved. diff --git a/chrome/renderer/BUILD.gn b/chrome/renderer/BUILD.gn index 7184fd42569ec8..f3191198cd004b 100644 --- a/chrome/renderer/BUILD.gn +++ b/chrome/renderer/BUILD.gn @@ -73,6 +73,8 @@ static_library("renderer") { "browser_exposed_renderer_interfaces.h", "cart/commerce_hint_agent.cc", "cart/commerce_hint_agent.h", + "chat/page_content_extractor.cc", + "chat/page_content_extractor.h", "chrome_content_renderer_client.cc", "chrome_content_renderer_client.h", "chrome_content_settings_agent_delegate.cc", @@ -143,6 +145,7 @@ static_library("renderer") { "//chrome/common", "//chrome/common:mojo_bindings", "//chrome/common/cart:mojo_bindings", + "//chrome/common/chat:mojo_bindings", "//chrome/common/net", "//chrome/common/search:mojo_bindings", "//chrome/services/speech/buildflags", diff --git a/chrome/renderer/chat/page_content_extractor.cc b/chrome/renderer/chat/page_content_extractor.cc new file mode 100644 index 00000000000000..d7ab4dfbc2a437 --- /dev/null +++ b/chrome/renderer/chat/page_content_extractor.cc @@ -0,0 +1,241 @@ +#include "page_content_extractor.h" + +#include +#include +#include +#include +#include +#include + +#include "base/containers/contains.h" +#include "base/metrics/histogram_macros.h" +#include "base/ranges/algorithm.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "chrome/common/chrome_isolated_world_ids.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_thread.h" +#include "services/metrics/public/cpp/ukm_builders.h" +#include "third_party/blink/public/platform/browser_interface_broker_proxy.h" +#include "third_party/blink/public/platform/scheduler/web_agent_group_scheduler.h" +#include "third_party/blink/public/platform/web_string.h" +#include "third_party/blink/public/web/web_local_frame.h" +#include "third_party/blink/public/web/web_script_source.h" +#include "ui/accessibility/ax_node.h" +#include "ui/accessibility/ax_tree.h" +#include "v8/include/v8-isolate.h" + +namespace ai_chat { +namespace { +static const ax::mojom::Role kContentParentRoles[]{ + ax::mojom::Role::kMain, + ax::mojom::Role::kArticle, +}; + +static const ax::mojom::Role kContentRoles[]{ + ax::mojom::Role::kHeading, + ax::mojom::Role::kParagraph, + ax::mojom::Role::kNote, +}; + +static const ax::mojom::Role kRolesToSkip[]{ + ax::mojom::Role::kAudio, ax::mojom::Role::kBanner, + ax::mojom::Role::kButton, ax::mojom::Role::kComplementary, + ax::mojom::Role::kContentInfo, ax::mojom::Role::kFooter, + ax::mojom::Role::kImage, ax::mojom::Role::kLabelText, + ax::mojom::Role::kNavigation, ax::mojom::Role::kSectionFooter, + ax::mojom::Role::kTextField, ax::mojom::Role::kTextFieldWithComboBox, + ax::mojom::Role::kComboBoxSelect, ax::mojom::Role::kListBox, + ax::mojom::Role::kListBoxOption, ax::mojom::Role::kCheckBox, + ax::mojom::Role::kRadioButton, ax::mojom::Role::kSlider, + ax::mojom::Role::kSpinButton, ax::mojom::Role::kSearchBox, +}; + +void GetContentRootNodes(const ui::AXNode* root, + std::vector* content_root_nodes) { + std::queue queue; + queue.push(root); + while (!queue.empty()) { + const ui::AXNode* node = queue.front(); + queue.pop(); + // If a main or article node is found, add it to the list of content root + // nodes and continue. Do not explore children for nested article nodes. + if (base::Contains(kContentParentRoles, node->GetRole())) { + content_root_nodes->push_back(node); + continue; + } + for (auto iter = node->UnignoredChildrenBegin(); + iter != node->UnignoredChildrenEnd(); ++iter) { + queue.push(iter.get()); + } + } +} + +void AddContentNodesToVector(const ui::AXNode* node, + std::vector* content_nodes) { + if (base::Contains(kContentRoles, node->GetRole())) { + content_nodes->emplace_back(node); + return; + } + if (base::Contains(kRolesToSkip, node->GetRole())) { + return; + } + for (auto iter = node->UnignoredChildrenBegin(); + iter != node->UnignoredChildrenEnd(); ++iter) { + AddContentNodesToVector(iter.get(), content_nodes); + } +} + +void AddTextNodesToVector(const ui::AXNode* node, + std::vector* strings) { + const ui::AXNodeData& node_data = node->data(); + + if (base::Contains(kRolesToSkip, node_data.role)) { + return; + } + + if (node_data.role == ax::mojom::Role::kStaticText) { + if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kName)) { + strings->push_back( + node_data.GetString16Attribute(ax::mojom::StringAttribute::kName)); + } + return; + } + + for (const ui::AXNode* child : node->children()) { + AddTextNodesToVector(child, strings); + } +} +} // namespace + +PageContentExtractor::PageContentExtractor( + content::RenderFrame* render_frame, + service_manager::BinderRegistry* registry, + int32_t isolated_world_id) + : content::RenderFrameObserver(render_frame), + RenderFrameObserverTracker(render_frame), + isolated_world_id_(isolated_world_id), + weak_ptr_factory_(this) { + if (!render_frame->IsMainFrame()) { + return; + } + registry->AddInterface(base::BindRepeating( + &PageContentExtractor::BindReceiver, base::Unretained(this))); +} + +PageContentExtractor::~PageContentExtractor() = default; + +void PageContentExtractor::OnDestruct() { + delete this; +} + +base::WeakPtr PageContentExtractor::GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); +} + +void PageContentExtractor::BindReceiver( + mojo::PendingReceiver receiver) { + DVLOG(0) << "Yep Chat PageContentExtractor handler bound."; + receiver_.reset(); + receiver_.Bind(std::move(receiver)); +} + +void PageContentExtractor::ExtractPageContent( + chat::mojom::PageContentExtractor::ExtractPageContentCallback callback) { + DVLOG(0) << __func__ << "The current page will be extracted for Yep Chat."; + ExtractPageText( + render_frame(), isolated_world_id_, + base::BindOnce(&PageContentExtractor::OnPageTextExtracted, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); +} + +void PageContentExtractor::ExtractPageText( + content::RenderFrame* render_frame, + int32_t isolated_world_id, + base::OnceCallback&, const std::optional&)> callback) { + auto snapshotter = render_frame->CreateAXTreeSnapshotter( + ui::AXMode::kWebContents | ui::AXMode::kHTML | ui::AXMode::kScreenReader); + ui::AXTreeUpdate snapshot; + snapshotter->Snapshot( + /* max_nodes= */ 9000, /* timeout= */ base::Seconds(4), &snapshot); + ui::AXTree tree(snapshot); + + std::vector content_root_nodes; + std::vector content_nodes; + GetContentRootNodes(tree.root(), &content_root_nodes); + + for (const ui::AXNode* content_root_node : content_root_nodes) { + std::vector content_nodes_this_root; + AddContentNodesToVector(content_root_node, &content_nodes_this_root); + // If no content was retrieved for this root node, fall back to using + // the text directly from the root node. This ensures we capture content + // from the node identified as containing important information. + if (content_nodes_this_root.empty()) { + content_nodes.emplace_back(content_root_node); + } else { + base::ranges::move(content_nodes_this_root, + std::back_inserter(content_nodes)); + } + } + + std::vector text_node_contents; + for (const ui::AXNode* content_node : content_nodes) { + AddTextNodesToVector(content_node, &text_node_contents); + } + + std::string contents_text = + base::UTF16ToUTF8(base::JoinString(text_node_contents, u" ")); + + blink::WebLocalFrame* main_frame = render_frame->GetWebFrame(); + // Retrieve the current URL + blink::WebURL web_url = main_frame->GetDocumentLoader()->GetUrl(); + + // Convert to GURL for convenience + GURL url(web_url); + +// if (url.is_valid()) { +// LOG(INFO) << "Current URL: " << url.spec(); +// } else { +// LOG(INFO) << "Invalid URL."; +// } + + if (contents_text.empty()) { + + v8::HandleScope handle_scope( + main_frame->GetAgentGroupScheduler()->Isolate()); + blink::WebScriptSource source = blink::WebScriptSource( + blink::WebString::FromASCII("document.body.innerText")); + + auto on_script_executed = + [](base::OnceCallback&, const std::optional&)> callback, + std::string url, std::optional value, base::TimeTicks start_time) { + if (value && value->is_string()) { + std::move(callback).Run(value->GetString(), url); + } else { + std::move(callback).Run("", ""); + } + }; + + render_frame->GetWebFrame()->RequestExecuteScript( + isolated_world_id, UNSAFE_TODO(base::make_span(&source, 1u)), + blink::mojom::UserActivationOption::kDoNotActivate, + blink::mojom::EvaluationTiming::kAsynchronous, + blink::mojom::LoadEventBlockingOption::kDoNotBlock, + base::BindOnce(on_script_executed, std::move(callback), url.spec()), + blink::BackForwardCacheAware::kAllow, + blink::mojom::WantResultOption::kWantResult, + blink::mojom::PromiseResultOption::kAwait); + } else { + // todo: to test this path + std::move(callback).Run(std::move(contents_text), std::move(url.spec())); + } +} + +void PageContentExtractor::OnPageTextExtracted( + chat::mojom::PageContentExtractor::ExtractPageContentCallback callback, + const std::optional& content, + const std::optional& url) { + std::move(callback).Run(std::move(content), std::move(url)); +} +} // namespace ai_chat diff --git a/chrome/renderer/chat/page_content_extractor.h b/chrome/renderer/chat/page_content_extractor.h new file mode 100644 index 00000000000000..42efe9e95836a8 --- /dev/null +++ b/chrome/renderer/chat/page_content_extractor.h @@ -0,0 +1,69 @@ +#ifndef CHROMIUM_PAGE_CONTENT_EXTRACTOR_H +#define CHROMIUM_PAGE_CONTENT_EXTRACTOR_H + +#include +#include +#include +#include + +#include "base/functional/callback_forward.h" +#include "base/values.h" +#include "chrome/common/chat/page_content_extractor.mojom.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_frame_observer.h" +#include "content/public/renderer/render_frame_observer_tracker.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/bindings/remote.h" + +namespace content { +class RenderFrame; +} + +namespace ai_chat { + +class PageContentExtractor + : public chat::mojom::PageContentExtractor, + public content::RenderFrameObserver, + public content::RenderFrameObserverTracker { + public: + PageContentExtractor(content::RenderFrame* render_frame, + service_manager::BinderRegistry* registry, + int32_t isolated_world_id); + + PageContentExtractor(const PageContentExtractor&) = delete; + PageContentExtractor& operator=(const PageContentExtractor&) = delete; + ~PageContentExtractor() override; + + base::WeakPtr GetWeakPtr(); + + private: + // chat::mojom::PageContentExtractor implementation: + void ExtractPageContent( + chat::mojom::PageContentExtractor::ExtractPageContentCallback callback) + override; + + // RenderFrameObserver implementation: + void OnDestruct() override; + + void BindReceiver( + mojo::PendingReceiver receiver); + + mojo::Receiver receiver_{this}; + + int32_t isolated_world_id_; + + void ExtractPageText( + content::RenderFrame* render_frame, + int32_t isolated_world_id, + base::OnceCallback& text, const std::optional& url)>); + + void OnPageTextExtracted( + chat::mojom::PageContentExtractor::ExtractPageContentCallback callback, + const std::optional& text, + const std::optional& url); + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace ai_chat +#endif // CHROMIUM_PAGE_CONTENT_EXTRACTOR_H diff --git a/chrome/renderer/chrome_content_renderer_client.cc b/chrome/renderer/chrome_content_renderer_client.cc index cc8af23c1e29de..a3dc3dfa0d6375 100644 --- a/chrome/renderer/chrome_content_renderer_client.cc +++ b/chrome/renderer/chrome_content_renderer_client.cc @@ -51,6 +51,7 @@ #include "chrome/renderer/benchmarking_extension.h" #include "chrome/renderer/browser_exposed_renderer_interfaces.h" #include "chrome/renderer/cart/commerce_hint_agent.h" +#include "chrome/renderer/chat/page_content_extractor.h" #include "chrome/renderer/chrome_content_settings_agent_delegate.h" #include "chrome/renderer/chrome_render_frame_observer.h" #include "chrome/renderer/chrome_render_thread_observer.h" @@ -63,6 +64,7 @@ #include "chrome/renderer/net_benchmarking_extension.h" #include "chrome/renderer/plugins/non_loadable_plugin_placeholder.h" #include "chrome/renderer/plugins/pdf_plugin_placeholder.h" +#include "chrome/renderer/process_state.h" #include "chrome/renderer/supervised_user/supervised_user_error_page_controller_delegate_impl.h" #include "chrome/renderer/trusted_vault_encryption_keys_extension.h" #include "chrome/renderer/url_loader_throttle_provider_impl.h" @@ -821,6 +823,13 @@ void ChromeContentRendererClient::RenderFrameCreated( new wallet::BoardingPassExtractor(render_frame, registry); } #endif + +#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) + if (render_frame->IsMainFrame() && !IsIncognitoProcess()) { + new ai_chat::PageContentExtractor(render_frame, registry, + ISOLATED_WORLD_ID_CHROME_INTERNAL); + } +#endif } void ChromeContentRendererClient::WebViewCreated( diff --git a/third_party/lit/v3_0/BUILD.gn b/third_party/lit/v3_0/BUILD.gn index 634d839caab86c..c7a3249b433dc0 100644 --- a/third_party/lit/v3_0/BUILD.gn +++ b/third_party/lit/v3_0/BUILD.gn @@ -49,6 +49,7 @@ ts_library("build_ts") { "//chrome/browser/resources/side_panel/history_clusters:build_ts", "//chrome/browser/resources/side_panel/read_anything:build_ts", "//chrome/browser/resources/side_panel/reading_list:build_ts", + "//chrome/browser/resources/side_panel/chat:build_ts", "//chrome/browser/resources/side_panel/shared:build_ts", "//chrome/browser/resources/signin:build_ts", "//chrome/browser/resources/signin/batch_upload:build_ts", diff --git a/third_party/marked/BUILD.gn b/third_party/marked/BUILD.gn new file mode 100644 index 00000000000000..344a97fdfde5b9 --- /dev/null +++ b/third_party/marked/BUILD.gn @@ -0,0 +1,9 @@ +import("//ui/webui/resources/tools/generate_grd.gni") + +generate_grd("build_grdp") { + grd_prefix = "marked" + out_grd = "$target_gen_dir/marked_resources.grdp" + input_files_base_dir = rebase_path("./src", "//") + input_files = [ "marked.min.js" ] + resource_path_prefix = "marked" +} diff --git a/third_party/marked/LICENSE.md b/third_party/marked/LICENSE.md new file mode 100644 index 00000000000000..4bd2d4a084987a --- /dev/null +++ b/third_party/marked/LICENSE.md @@ -0,0 +1,44 @@ +# License information + +## Contribution License Agreement + +If you contribute code to this project, you are implicitly allowing your code +to be distributed under the MIT license. You are also implicitly verifying that +all code is your original work. `` + +## Marked + +Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/) +Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +## Markdown + +Copyright © 2004, John Gruber +http://daringfireball.net/ +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +* Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. diff --git a/third_party/marked/README.chromium b/third_party/marked/README.chromium new file mode 100644 index 00000000000000..87d397dede7c9b --- /dev/null +++ b/third_party/marked/README.chromium @@ -0,0 +1,17 @@ +Name: MarkedJS +Short Name: markedjs +URL: https://github.com/markedjs/marked +Version: v15.0.3 +Revision: 7e4e3435eaca2b0f48f5aedb53d67a36170086f3 +License: MIT +License File: LICENSE +Security Critical: no +Shipped: yes + +Description: +low-level compiler for parsing markdown without caching or blocking for long periods of time + +Local Modifications: +Copy the entire file into Chat and use as it. + + diff --git a/third_party/marked/src/marked.js b/third_party/marked/src/marked.js new file mode 100644 index 00000000000000..36ffc33e436494 --- /dev/null +++ b/third_party/marked/src/marked.js @@ -0,0 +1,2553 @@ +/** + * marked v15.0.3 - a markdown parser + * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ + +/** + * DO NOT EDIT THIS FILE + * The code in this file is generated from files in ./src/ + */ + +/** + * Gets the original marked default options. + */ +function _getDefaults() { + return { + async: false, + breaks: false, + extensions: null, + gfm: true, + hooks: null, + pedantic: false, + renderer: null, + silent: false, + tokenizer: null, + walkTokens: null, + }; +} +let _defaults = _getDefaults(); +function changeDefaults(newDefaults) { + _defaults = newDefaults; +} + +const noopTest = { exec: () => null }; +function edit(regex, opt = '') { + let source = typeof regex === 'string' ? regex : regex.source; + const obj = { + replace: (name, val) => { + let valSource = typeof val === 'string' ? val : val.source; + valSource = valSource.replace(other.caret, '$1'); + source = source.replace(name, valSource); + return obj; + }, + getRegex: () => { + return new RegExp(source, opt); + }, + }; + return obj; +} +const other = { + codeRemoveIndent: /^(?: {1,4}| {0,3}\t)/gm, + outputLinkReplace: /\\([\[\]])/g, + indentCodeCompensation: /^(\s+)(?:```)/, + beginningSpace: /^\s+/, + endingHash: /#$/, + startingSpaceChar: /^ /, + endingSpaceChar: / $/, + nonSpaceChar: /[^ ]/, + newLineCharGlobal: /\n/g, + tabCharGlobal: /\t/g, + multipleSpaceGlobal: /\s+/g, + blankLine: /^[ \t]*$/, + doubleBlankLine: /\n[ \t]*\n[ \t]*$/, + blockquoteStart: /^ {0,3}>/, + blockquoteSetextReplace: /\n {0,3}((?:=+|-+) *)(?=\n|$)/g, + blockquoteSetextReplace2: /^ {0,3}>[ \t]?/gm, + listReplaceTabs: /^\t+/, + listReplaceNesting: /^ {1,4}(?=( {4})*[^ ])/g, + listIsTask: /^\[[ xX]\] /, + listReplaceTask: /^\[[ xX]\] +/, + anyLine: /\n.*\n/, + hrefBrackets: /^<(.*)>$/, + tableDelimiter: /[:|]/, + tableAlignChars: /^\||\| *$/g, + tableRowBlankLine: /\n[ \t]*$/, + tableAlignRight: /^ *-+: *$/, + tableAlignCenter: /^ *:-+: *$/, + tableAlignLeft: /^ *:-+ *$/, + startATag: /^
    /i, + startPreScriptTag: /^<(pre|code|kbd|script)(\s|>)/i, + endPreScriptTag: /^<\/(pre|code|kbd|script)(\s|>)/i, + startAngleBracket: /^$/, + pedanticHrefTitle: /^([^'"]*[^\s])\s+(['"])(.*)\2/, + unicodeAlphaNumeric: /[\p{L}\p{N}]/u, + escapeTest: /[&<>"']/, + escapeReplace: /[&<>"']/g, + escapeTestNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/, + escapeReplaceNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g, + unescapeTest: /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, + caret: /(^|[^\[])\^/g, + percentDecode: /%25/g, + findPipe: /\|/g, + splitPipe: / \|/, + slashPipe: /\\\|/g, + carriageReturn: /\r\n|\r/g, + spaceLine: /^ +$/gm, + notSpaceStart: /^\S*/, + endingNewline: /\n$/, + listItemRegex: (bull) => new RegExp(`^( {0,3}${bull})((?:[\t ][^\\n]*)?(?:\\n|$))`), + nextBulletRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`), + hrRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`), + fencesBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:\`\`\`|~~~)`), + headingBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}#`), + htmlBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}<(?:[a-z].*>|!--)`, 'i'), +}; +/** + * Block-Level Grammar + */ +const newline = /^(?:[ \t]*(?:\n|$))+/; +const blockCode = /^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/; +const fences = /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/; +const hr = /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/; +const heading = /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/; +const bullet = /(?:[*+-]|\d{1,9}[.)])/; +const lheading = edit(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/) + .replace(/bull/g, bullet) // lists can interrupt + .replace(/blockCode/g, /(?: {4}| {0,3}\t)/) // indented code blocks can interrupt + .replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/) // fenced code blocks can interrupt + .replace(/blockquote/g, / {0,3}>/) // blockquote can interrupt + .replace(/heading/g, / {0,3}#{1,6}/) // ATX heading can interrupt + .replace(/html/g, / {0,3}<[^\n>]+>\n/) // block html can interrupt + .getRegex(); +const _paragraph = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/; +const blockText = /^[^\n]+/; +const _blockLabel = /(?!\s*\])(?:\\.|[^\[\]\\])+/; +const def = edit(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/) + .replace('label', _blockLabel) + .replace('title', /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/) + .getRegex(); +const list = edit(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/) + .replace(/bull/g, bullet) + .getRegex(); +const _tag = 'address|article|aside|base|basefont|blockquote|body|caption' + + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption' + + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe' + + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option' + + '|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title' + + '|tr|track|ul'; +const _comment = /|$))/; +const html = edit('^ {0,3}(?:' // optional indentation + + '<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)' // (1) + + '|comment[^\\n]*(\\n+|$)' // (2) + + '|<\\?[\\s\\S]*?(?:\\?>\\n*|$)' // (3) + + '|\\n*|$)' // (4) + + '|\\n*|$)' // (5) + + '|)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (6) + + '|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (7) open tag + + '|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (7) closing tag + + ')', 'i') + .replace('comment', _comment) + .replace('tag', _tag) + .replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/) + .getRegex(); +const paragraph = edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs + .replace('|table', '') + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // pars can be interrupted by type (6) html blocks + .getRegex(); +const blockquote = edit(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/) + .replace('paragraph', paragraph) + .getRegex(); +/** + * Normal Block Grammar + */ +const blockNormal = { + blockquote, + code: blockCode, + def, + fences, + heading, + hr, + html, + lheading, + list, + newline, + paragraph, + table: noopTest, + text: blockText, +}; +/** + * GFM Block Grammar + */ +const gfmTable = edit('^ *([^\\n ].*)\\n' // Header + + ' {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)' // Align + + '(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)') // Cells + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('blockquote', ' {0,3}>') + .replace('code', '(?: {4}| {0,3}\t)[^\\n]') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // tables can be interrupted by type (6) html blocks + .getRegex(); +const blockGfm = { + ...blockNormal, + table: gfmTable, + paragraph: edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs + .replace('table', gfmTable) // interrupt paragraphs with table + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // pars can be interrupted by type (6) html blocks + .getRegex(), +}; +/** + * Pedantic grammar (original John Gruber's loose markdown specification) + */ +const blockPedantic = { + ...blockNormal, + html: edit('^ *(?:comment *(?:\\n|\\s*$)' + + '|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)' // closed tag + + '|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))') + .replace('comment', _comment) + .replace(/tag/g, '(?!(?:' + + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub' + + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)' + + '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b') + .getRegex(), + def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, + heading: /^(#{1,6})(.*)(?:\n+|$)/, + fences: noopTest, // fences not supported + lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, + paragraph: edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' *#{1,6} *[^\n]') + .replace('lheading', lheading) + .replace('|table', '') + .replace('blockquote', ' {0,3}>') + .replace('|fences', '') + .replace('|list', '') + .replace('|html', '') + .replace('|tag', '') + .getRegex(), +}; +/** + * Inline-Level Grammar + */ +const escape$1 = /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/; +const inlineCode = /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/; +const br = /^( {2,}|\\)\n(?!\s*$)/; +const inlineText = /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\ +const blockSkip = /\[[^[\]]*?\]\((?:\\.|[^\\\(\)]|\((?:\export as namespace d3; +\.|[^\\\(\)])*\))*\)|`[^`]*?`|<[^<>]*?>/g; +const emStrongLDelim = edit(/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/, 'u') + .replace(/punct/g, _punctuation) + .getRegex(); +const emStrongRDelimAst = edit('^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)' // Skip orphan inside strong + + '|[^*]+(?=[^*])' // Consume to delim + + '|(?!\\*)punct(\\*+)(?=[\\s]|$)' // (1) #*** can only be a Right Delimiter + + '|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)' // (2) a***#, a*** can only be a Right Delimiter + + '|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)' // (3) #***a, ***a can only be Left Delimiter + + '|[\\s](\\*+)(?!\\*)(?=punct)' // (4) ***# can only be Left Delimiter + + '|(?!\\*)punct(\\*+)(?!\\*)(?=punct)' // (5) #***# can be either Left or Right Delimiter + + '|notPunctSpace(\\*+)(?=notPunctSpace)', 'gu') // (6) a***a can be either Left or Right Delimiter + .replace(/notPunctSpace/g, _notPunctuationOrSpace) + .replace(/punctSpace/g, _punctuationOrSpace) + .replace(/punct/g, _punctuation) + .getRegex(); +// (6) Not allowed for _ +const emStrongRDelimUnd = edit('^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)' // Skip orphan inside strong + + '|[^_]+(?=[^_])' // Consume to delim + + '|(?!_)punct(_+)(?=[\\s]|$)' // (1) #___ can only be a Right Delimiter + + '|notPunctSpace(_+)(?!_)(?=punctSpace|$)' // (2) a___#, a___ can only be a Right Delimiter + + '|(?!_)punctSpace(_+)(?=notPunctSpace)' // (3) #___a, ___a can only be Left Delimiter + + '|[\\s](_+)(?!_)(?=punct)' // (4) ___# can only be Left Delimiter + + '|(?!_)punct(_+)(?!_)(?=punct)', 'gu') // (5) #___# can be either Left or Right Delimiter + .replace(/notPunctSpace/g, _notPunctuationOrSpace) + .replace(/punctSpace/g, _punctuationOrSpace) + .replace(/punct/g, _punctuation) + .getRegex(); +const anyPunctuation = edit(/\\(punct)/, 'gu') + .replace(/punct/g, _punctuation) + .getRegex(); +const autolink = edit(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/) + .replace('scheme', /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/) + .replace('email', /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/) + .getRegex(); +const _inlineComment = edit(_comment).replace('(?:-->|$)', '-->').getRegex(); +const tag = edit('^comment' + + '|^' // self-closing tag + + '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag + + '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. + + '|^' // declaration, e.g. + + '|^') // CDATA section + .replace('comment', _inlineComment) + .replace('attribute', /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/) + .getRegex(); +const _inlineLabel = /(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/; +const link = edit(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/) + .replace('label', _inlineLabel) + .replace('href', /<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/) + .replace('title', /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/) + .getRegex(); +const reflink = edit(/^!?\[(label)\]\[(ref)\]/) + .replace('label', _inlineLabel) + .replace('ref', _blockLabel) + .getRegex(); +const nolink = edit(/^!?\[(ref)\](?:\[\])?/) + .replace('ref', _blockLabel) + .getRegex(); +const reflinkSearch = edit('reflink|nolink(?!\\()', 'g') + .replace('reflink', reflink) + .replace('nolink', nolink) + .getRegex(); +/** + * Normal Inline Grammar + */ +const inlineNormal = { + _backpedal: noopTest, // only used for GFM url + anyPunctuation, + autolink, + blockSkip, + br, + code: inlineCode, + del: noopTest, + emStrongLDelim, + emStrongRDelimAst, + emStrongRDelimUnd, + escape: escape$1, + link, + nolink, + punctuation, + reflink, + reflinkSearch, + tag, + text: inlineText, + url: noopTest, +}; +/** + * Pedantic Inline Grammar + */ +const inlinePedantic = { + ...inlineNormal, + link: edit(/^!?\[(label)\]\((.*?)\)/) + .replace('label', _inlineLabel) + .getRegex(), + reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/) + .replace('label', _inlineLabel) + .getRegex(), +}; +/** + * GFM Inline Grammar + */ +const inlineGfm = { + ...inlineNormal, + escape: edit(escape$1).replace('])', '~|])').getRegex(), + url: edit(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/, 'i') + .replace('email', /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/) + .getRegex(), + _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, + del: /^(~~?)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/, + text: /^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\': '>', + '"': '"', + "'": ''', +}; +const getEscapeReplacement = (ch) => escapeReplacements[ch]; +function escape(html, encode) { + if (encode) { + if (other.escapeTest.test(html)) { + return html.replace(other.escapeReplace, getEscapeReplacement); + } + } + else { + if (other.escapeTestNoEncode.test(html)) { + return html.replace(other.escapeReplaceNoEncode, getEscapeReplacement); + } + } + return html; +} +function cleanUrl(href) { + try { + href = encodeURI(href).replace(other.percentDecode, '%'); + } + catch { + return null; + } + return href; +} +function splitCells(tableRow, count) { + // ensure that every cell-delimiting pipe has a space + // before it to distinguish it from an escaped pipe + const row = tableRow.replace(other.findPipe, (match, offset, str) => { + let escaped = false; + let curr = offset; + while (--curr >= 0 && str[curr] === '\\') + escaped = !escaped; + if (escaped) { + // odd number of slashes means | is escaped + // so we leave it alone + return '|'; + } + else { + // add space before unescaped | + return ' |'; + } + }), cells = row.split(other.splitPipe); + let i = 0; + // First/last cell in a row cannot be empty if it has no leading/trailing pipe + if (!cells[0].trim()) { + cells.shift(); + } + if (cells.length > 0 && !cells.at(-1)?.trim()) { + cells.pop(); + } + if (count) { + if (cells.length > count) { + cells.splice(count); + } + else { + while (cells.length < count) + cells.push(''); + } + } + for (; i < cells.length; i++) { + // leading or trailing whitespace is ignored per the gfm spec + cells[i] = cells[i].trim().replace(other.slashPipe, '|'); + } + return cells; +} +/** + * Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). + * /c*$/ is vulnerable to REDOS. + * + * @param str + * @param c + * @param invert Remove suffix of non-c chars instead. Default falsey. + */ +function rtrim(str, c, invert) { + const l = str.length; + if (l === 0) { + return ''; + } + // Length of suffix matching the invert condition. + let suffLen = 0; + // Step left until we fail to match the invert condition. + while (suffLen < l) { + const currChar = str.charAt(l - suffLen - 1); + if (currChar === c && !invert) { + suffLen++; + } + else if (currChar !== c && invert) { + suffLen++; + } + else { + break; + } + } + return str.slice(0, l - suffLen); +} +function findClosingBracket(str, b) { + if (str.indexOf(b[1]) === -1) { + return -1; + } + let level = 0; + for (let i = 0; i < str.length; i++) { + if (str[i] === '\\') { + i++; + } + else if (str[i] === b[0]) { + level++; + } + else if (str[i] === b[1]) { + level--; + if (level < 0) { + return i; + } + } + } + return -1; +} + +function outputLink(cap, link, raw, lexer, rules) { + const href = link.href; + const title = link.title || null; + const text = cap[1].replace(rules.other.outputLinkReplace, '$1'); + if (cap[0].charAt(0) !== '!') { + lexer.state.inLink = true; + const token = { + type: 'link', + raw, + href, + title, + text, + tokens: lexer.inlineTokens(text), + }; + lexer.state.inLink = false; + return token; + } + return { + type: 'image', + raw, + href, + title, + text, + }; +} +function indentCodeCompensation(raw, text, rules) { + const matchIndentToCode = raw.match(rules.other.indentCodeCompensation); + if (matchIndentToCode === null) { + return text; + } + const indentToCode = matchIndentToCode[1]; + return text + .split('\n') + .map(node => { + const matchIndentInNode = node.match(rules.other.beginningSpace); + if (matchIndentInNode === null) { + return node; + } + const [indentInNode] = matchIndentInNode; + if (indentInNode.length >= indentToCode.length) { + return node.slice(indentToCode.length); + } + return node; + }) + .join('\n'); +} +/** + * Tokenizer + */ +class _Tokenizer { + options; + rules; // set by the lexer + lexer; // set by the lexer + constructor(options) { + this.options = options || _defaults; + } + space(src) { + const cap = this.rules.block.newline.exec(src); + if (cap && cap[0].length > 0) { + return { + type: 'space', + raw: cap[0], + }; + } + } + code(src) { + const cap = this.rules.block.code.exec(src); + if (cap) { + const text = cap[0].replace(this.rules.other.codeRemoveIndent, ''); + return { + type: 'code', + raw: cap[0], + codeBlockStyle: 'indented', + text: !this.options.pedantic + ? rtrim(text, '\n') + : text, + }; + } + } + fences(src) { + const cap = this.rules.block.fences.exec(src); + if (cap) { + const raw = cap[0]; + const text = indentCodeCompensation(raw, cap[3] || '', this.rules); + return { + type: 'code', + raw, + lang: cap[2] ? cap[2].trim().replace(this.rules.inline.anyPunctuation, '$1') : cap[2], + text, + }; + } + } + heading(src) { + const cap = this.rules.block.heading.exec(src); + if (cap) { + let text = cap[2].trim(); + // remove trailing #s + if (this.rules.other.endingHash.test(text)) { + const trimmed = rtrim(text, '#'); + if (this.options.pedantic) { + text = trimmed.trim(); + } + else if (!trimmed || this.rules.other.endingSpaceChar.test(trimmed)) { + // CommonMark requires space before trailing #s + text = trimmed.trim(); + } + } + return { + type: 'heading', + raw: cap[0], + depth: cap[1].length, + text, + tokens: this.lexer.inline(text), + }; + } + } + hr(src) { + const cap = this.rules.block.hr.exec(src); + if (cap) { + return { + type: 'hr', + raw: rtrim(cap[0], '\n'), + }; + } + } + blockquote(src) { + const cap = this.rules.block.blockquote.exec(src); + if (cap) { + let lines = rtrim(cap[0], '\n').split('\n'); + let raw = ''; + let text = ''; + const tokens = []; + while (lines.length > 0) { + let inBlockquote = false; + const currentLines = []; + let i; + for (i = 0; i < lines.length; i++) { + // get lines up to a continuation + if (this.rules.other.blockquoteStart.test(lines[i])) { + currentLines.push(lines[i]); + inBlockquote = true; + } + else if (!inBlockquote) { + currentLines.push(lines[i]); + } + else { + break; + } + } + lines = lines.slice(i); + const currentRaw = currentLines.join('\n'); + const currentText = currentRaw + // precede setext continuation with 4 spaces so it isn't a setext + .replace(this.rules.other.blockquoteSetextReplace, '\n $1') + .replace(this.rules.other.blockquoteSetextReplace2, ''); + raw = raw ? `${raw}\n${currentRaw}` : currentRaw; + text = text ? `${text}\n${currentText}` : currentText; + // parse blockquote lines as top level tokens + // merge paragraphs if this is a continuation + const top = this.lexer.state.top; + this.lexer.state.top = true; + this.lexer.blockTokens(currentText, tokens, true); + this.lexer.state.top = top; + // if there is no continuation then we are done + if (lines.length === 0) { + break; + } + const lastToken = tokens.at(-1); + if (lastToken?.type === 'code') { + // blockquote continuation cannot be preceded by a code block + break; + } + else if (lastToken?.type === 'blockquote') { + // include continuation in nested blockquote + const oldToken = lastToken; + const newText = oldToken.raw + '\n' + lines.join('\n'); + const newToken = this.blockquote(newText); + tokens[tokens.length - 1] = newToken; + raw = raw.substring(0, raw.length - oldToken.raw.length) + newToken.raw; + text = text.substring(0, text.length - oldToken.text.length) + newToken.text; + break; + } + else if (lastToken?.type === 'list') { + // include continuation in nested list + const oldToken = lastToken; + const newText = oldToken.raw + '\n' + lines.join('\n'); + const newToken = this.list(newText); + tokens[tokens.length - 1] = newToken; + raw = raw.substring(0, raw.length - lastToken.raw.length) + newToken.raw; + text = text.substring(0, text.length - oldToken.raw.length) + newToken.raw; + lines = newText.substring(tokens.at(-1).raw.length).split('\n'); + continue; + } + } + return { + type: 'blockquote', + raw, + tokens, + text, + }; + } + } + list(src) { + let cap = this.rules.block.list.exec(src); + if (cap) { + let bull = cap[1].trim(); + const isordered = bull.length > 1; + const list = { + type: 'list', + raw: '', + ordered: isordered, + start: isordered ? +bull.slice(0, -1) : '', + loose: false, + items: [], + }; + bull = isordered ? `\\d{1,9}\\${bull.slice(-1)}` : `\\${bull}`; + if (this.options.pedantic) { + bull = isordered ? bull : '[*+-]'; + } + // Get next list item + const itemRegex = this.rules.other.listItemRegex(bull); + let endsWithBlankLine = false; + // Check if current bullet point can start a new List Item + while (src) { + let endEarly = false; + let raw = ''; + let itemContents = ''; + if (!(cap = itemRegex.exec(src))) { + break; + } + if (this.rules.block.hr.test(src)) { // End list if bullet was actually HR (possibly move into itemRegex?) + break; + } + raw = cap[0]; + src = src.substring(raw.length); + let line = cap[2].split('\n', 1)[0].replace(this.rules.other.listReplaceTabs, (t) => ' '.repeat(3 * t.length)); + let nextLine = src.split('\n', 1)[0]; + let blankLine = !line.trim(); + let indent = 0; + if (this.options.pedantic) { + indent = 2; + itemContents = line.trimStart(); + } + else if (blankLine) { + indent = cap[1].length + 1; + } + else { + indent = cap[2].search(this.rules.other.nonSpaceChar); // Find first non-space char + indent = indent > 4 ? 1 : indent; // Treat indented code blocks (> 4 spaces) as having only 1 indent + itemContents = line.slice(indent); + indent += cap[1].length; + } + if (blankLine && this.rules.other.blankLine.test(nextLine)) { // Items begin with at most one blank line + raw += nextLine + '\n'; + src = src.substring(nextLine.length + 1); + endEarly = true; + } + if (!endEarly) { + const nextBulletRegex = this.rules.other.nextBulletRegex(indent); + const hrRegex = this.rules.other.hrRegex(indent); + const fencesBeginRegex = this.rules.other.fencesBeginRegex(indent); + const headingBeginRegex = this.rules.other.headingBeginRegex(indent); + const htmlBeginRegex = this.rules.other.htmlBeginRegex(indent); + // Check if following lines should be included in List Item + while (src) { + const rawLine = src.split('\n', 1)[0]; + let nextLineWithoutTabs; + nextLine = rawLine; + // Re-align to follow commonmark nesting rules + if (this.options.pedantic) { + nextLine = nextLine.replace(this.rules.other.listReplaceNesting, ' '); + nextLineWithoutTabs = nextLine; + } + else { + nextLineWithoutTabs = nextLine.replace(this.rules.other.tabCharGlobal, ' '); + } + // End list item if found code fences + if (fencesBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of new heading + if (headingBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of html block + if (htmlBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of new bullet + if (nextBulletRegex.test(nextLine)) { + break; + } + // Horizontal rule found + if (hrRegex.test(nextLine)) { + break; + } + if (nextLineWithoutTabs.search(this.rules.other.nonSpaceChar) >= indent || !nextLine.trim()) { // Dedent if possible + itemContents += '\n' + nextLineWithoutTabs.slice(indent); + } + else { + // not enough indentation + if (blankLine) { + break; + } + // paragraph continuation unless last line was a different block level element + if (line.replace(this.rules.other.tabCharGlobal, ' ').search(this.rules.other.nonSpaceChar) >= 4) { // indented code block + break; + } + if (fencesBeginRegex.test(line)) { + break; + } + if (headingBeginRegex.test(line)) { + break; + } + if (hrRegex.test(line)) { + break; + } + itemContents += '\n' + nextLine; + } + if (!blankLine && !nextLine.trim()) { // Check if current line is blank + blankLine = true; + } + raw += rawLine + '\n'; + src = src.substring(rawLine.length + 1); + line = nextLineWithoutTabs.slice(indent); + } + } + if (!list.loose) { + // If the previous item ended with a blank line, the list is loose + if (endsWithBlankLine) { + list.loose = true; + } + else if (this.rules.other.doubleBlankLine.test(raw)) { + endsWithBlankLine = true; + } + } + let istask = null; + let ischecked; + // Check for task list items + if (this.options.gfm) { + istask = this.rules.other.listIsTask.exec(itemContents); + if (istask) { + ischecked = istask[0] !== '[ ] '; + itemContents = itemContents.replace(this.rules.other.listReplaceTask, ''); + } + } + list.items.push({ + type: 'list_item', + raw, + task: !!istask, + checked: ischecked, + loose: false, + text: itemContents, + tokens: [], + }); + list.raw += raw; + } + // Do not consume newlines at end of final item. Alternatively, make itemRegex *start* with any newlines to simplify/speed up endsWithBlankLine logic + const lastItem = list.items.at(-1); + if (lastItem) { + lastItem.raw = lastItem.raw.trimEnd(); + lastItem.text = lastItem.text.trimEnd(); + } + list.raw = list.raw.trimEnd(); + // Item child tokens handled here at end because we needed to have the final item to trim it first + for (let i = 0; i < list.items.length; i++) { + this.lexer.state.top = false; + list.items[i].tokens = this.lexer.blockTokens(list.items[i].text, []); + if (!list.loose) { + // Check if list should be loose + const spacers = list.items[i].tokens.filter(t => t.type === 'space'); + const hasMultipleLineBreaks = spacers.length > 0 && spacers.some(t => this.rules.other.anyLine.test(t.raw)); + list.loose = hasMultipleLineBreaks; + } + } + // Set all items to loose if list is loose + if (list.loose) { + for (let i = 0; i < list.items.length; i++) { + list.items[i].loose = true; + } + } + return list; + } + } + html(src) { + const cap = this.rules.block.html.exec(src); + if (cap) { + const token = { + type: 'html', + block: true, + raw: cap[0], + pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style', + text: cap[0], + }; + return token; + } + } + def(src) { + const cap = this.rules.block.def.exec(src); + if (cap) { + const tag = cap[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal, ' '); + const href = cap[2] ? cap[2].replace(this.rules.other.hrefBrackets, '$1').replace(this.rules.inline.anyPunctuation, '$1') : ''; + const title = cap[3] ? cap[3].substring(1, cap[3].length - 1).replace(this.rules.inline.anyPunctuation, '$1') : cap[3]; + return { + type: 'def', + tag, + raw: cap[0], + href, + title, + }; + } + } + table(src) { + const cap = this.rules.block.table.exec(src); + if (!cap) { + return; + } + if (!this.rules.other.tableDelimiter.test(cap[2])) { + // delimiter row must have a pipe (|) or colon (:) otherwise it is a setext heading + return; + } + const headers = splitCells(cap[1]); + const aligns = cap[2].replace(this.rules.other.tableAlignChars, '').split('|'); + const rows = cap[3]?.trim() ? cap[3].replace(this.rules.other.tableRowBlankLine, '').split('\n') : []; + const item = { + type: 'table', + raw: cap[0], + header: [], + align: [], + rows: [], + }; + if (headers.length !== aligns.length) { + // header and align columns must be equal, rows can be different. + return; + } + for (const align of aligns) { + if (this.rules.other.tableAlignRight.test(align)) { + item.align.push('right'); + } + else if (this.rules.other.tableAlignCenter.test(align)) { + item.align.push('center'); + } + else if (this.rules.other.tableAlignLeft.test(align)) { + item.align.push('left'); + } + else { + item.align.push(null); + } + } + for (let i = 0; i < headers.length; i++) { + item.header.push({ + text: headers[i], + tokens: this.lexer.inline(headers[i]), + header: true, + align: item.align[i], + }); + } + for (const row of rows) { + item.rows.push(splitCells(row, item.header.length).map((cell, i) => { + return { + text: cell, + tokens: this.lexer.inline(cell), + header: false, + align: item.align[i], + }; + })); + } + return item; + } + lheading(src) { + const cap = this.rules.block.lheading.exec(src); + if (cap) { + return { + type: 'heading', + raw: cap[0], + depth: cap[2].charAt(0) === '=' ? 1 : 2, + text: cap[1], + tokens: this.lexer.inline(cap[1]), + }; + } + } + paragraph(src) { + const cap = this.rules.block.paragraph.exec(src); + if (cap) { + const text = cap[1].charAt(cap[1].length - 1) === '\n' + ? cap[1].slice(0, -1) + : cap[1]; + return { + type: 'paragraph', + raw: cap[0], + text, + tokens: this.lexer.inline(text), + }; + } + } + text(src) { + const cap = this.rules.block.text.exec(src); + if (cap) { + return { + type: 'text', + raw: cap[0], + text: cap[0], + tokens: this.lexer.inline(cap[0]), + }; + } + } + escape(src) { + const cap = this.rules.inline.escape.exec(src); + if (cap) { + return { + type: 'escape', + raw: cap[0], + text: cap[1], + }; + } + } + tag(src) { + const cap = this.rules.inline.tag.exec(src); + if (cap) { + if (!this.lexer.state.inLink && this.rules.other.startATag.test(cap[0])) { + this.lexer.state.inLink = true; + } + else if (this.lexer.state.inLink && this.rules.other.endATag.test(cap[0])) { + this.lexer.state.inLink = false; + } + if (!this.lexer.state.inRawBlock && this.rules.other.startPreScriptTag.test(cap[0])) { + this.lexer.state.inRawBlock = true; + } + else if (this.lexer.state.inRawBlock && this.rules.other.endPreScriptTag.test(cap[0])) { + this.lexer.state.inRawBlock = false; + } + return { + type: 'html', + raw: cap[0], + inLink: this.lexer.state.inLink, + inRawBlock: this.lexer.state.inRawBlock, + block: false, + text: cap[0], + }; + } + } + link(src) { + const cap = this.rules.inline.link.exec(src); + if (cap) { + const trimmedUrl = cap[2].trim(); + if (!this.options.pedantic && this.rules.other.startAngleBracket.test(trimmedUrl)) { + // commonmark requires matching angle brackets + if (!(this.rules.other.endAngleBracket.test(trimmedUrl))) { + return; + } + // ending angle bracket cannot be escaped + const rtrimSlash = rtrim(trimmedUrl.slice(0, -1), '\\'); + if ((trimmedUrl.length - rtrimSlash.length) % 2 === 0) { + return; + } + } + else { + // find closing parenthesis + const lastParenIndex = findClosingBracket(cap[2], '()'); + if (lastParenIndex > -1) { + const start = cap[0].indexOf('!') === 0 ? 5 : 4; + const linkLen = start + cap[1].length + lastParenIndex; + cap[2] = cap[2].substring(0, lastParenIndex); + cap[0] = cap[0].substring(0, linkLen).trim(); + cap[3] = ''; + } + } + let href = cap[2]; + let title = ''; + if (this.options.pedantic) { + // split pedantic href and title + const link = this.rules.other.pedanticHrefTitle.exec(href); + if (link) { + href = link[1]; + title = link[3]; + } + } + else { + title = cap[3] ? cap[3].slice(1, -1) : ''; + } + href = href.trim(); + if (this.rules.other.startAngleBracket.test(href)) { + if (this.options.pedantic && !(this.rules.other.endAngleBracket.test(trimmedUrl))) { + // pedantic allows starting angle bracket without ending angle bracket + href = href.slice(1); + } + else { + href = href.slice(1, -1); + } + } + return outputLink(cap, { + href: href ? href.replace(this.rules.inline.anyPunctuation, '$1') : href, + title: title ? title.replace(this.rules.inline.anyPunctuation, '$1') : title, + }, cap[0], this.lexer, this.rules); + } + } + reflink(src, links) { + let cap; + if ((cap = this.rules.inline.reflink.exec(src)) + || (cap = this.rules.inline.nolink.exec(src))) { + const linkString = (cap[2] || cap[1]).replace(this.rules.other.multipleSpaceGlobal, ' '); + const link = links[linkString.toLowerCase()]; + if (!link) { + const text = cap[0].charAt(0); + return { + type: 'text', + raw: text, + text, + }; + } + return outputLink(cap, link, cap[0], this.lexer, this.rules); + } + } + emStrong(src, maskedSrc, prevChar = '') { + let match = this.rules.inline.emStrongLDelim.exec(src); + if (!match) + return; + // _ can't be between two alphanumerics. \p{L}\p{N} includes non-english alphabet/numbers as well + if (match[3] && prevChar.match(this.rules.other.unicodeAlphaNumeric)) + return; + const nextChar = match[1] || match[2] || ''; + if (!nextChar || !prevChar || this.rules.inline.punctuation.exec(prevChar)) { + // unicode Regex counts emoji as 1 char; spread into array for proper count (used multiple times below) + const lLength = [...match[0]].length - 1; + let rDelim, rLength, delimTotal = lLength, midDelimTotal = 0; + const endReg = match[0][0] === '*' ? this.rules.inline.emStrongRDelimAst : this.rules.inline.emStrongRDelimUnd; + endReg.lastIndex = 0; + // Clip maskedSrc to same section of string as src (move to lexer?) + maskedSrc = maskedSrc.slice(-1 * src.length + lLength); + while ((match = endReg.exec(maskedSrc)) != null) { + rDelim = match[1] || match[2] || match[3] || match[4] || match[5] || match[6]; + if (!rDelim) + continue; // skip single * in __abc*abc__ + rLength = [...rDelim].length; + if (match[3] || match[4]) { // found another Left Delim + delimTotal += rLength; + continue; + } + else if (match[5] || match[6]) { // either Left or Right Delim + if (lLength % 3 && !((lLength + rLength) % 3)) { + midDelimTotal += rLength; + continue; // CommonMark Emphasis Rules 9-10 + } + } + delimTotal -= rLength; + if (delimTotal > 0) + continue; // Haven't found enough closing delimiters + // Remove extra characters. *a*** -> *a* + rLength = Math.min(rLength, rLength + delimTotal + midDelimTotal); + // char length can be >1 for unicode characters; + const lastCharLength = [...match[0]][0].length; + const raw = src.slice(0, lLength + match.index + lastCharLength + rLength); + // Create `em` if smallest delimiter has odd char count. *a*** + if (Math.min(lLength, rLength) % 2) { + const text = raw.slice(1, -1); + return { + type: 'em', + raw, + text, + tokens: this.lexer.inlineTokens(text), + }; + } + // Create 'strong' if smallest delimiter has even char count. **a*** + const text = raw.slice(2, -2); + return { + type: 'strong', + raw, + text, + tokens: this.lexer.inlineTokens(text), + }; + } + } + } + codespan(src) { + const cap = this.rules.inline.code.exec(src); + if (cap) { + let text = cap[2].replace(this.rules.other.newLineCharGlobal, ' '); + const hasNonSpaceChars = this.rules.other.nonSpaceChar.test(text); + const hasSpaceCharsOnBothEnds = this.rules.other.startingSpaceChar.test(text) && this.rules.other.endingSpaceChar.test(text); + if (hasNonSpaceChars && hasSpaceCharsOnBothEnds) { + text = text.substring(1, text.length - 1); + } + return { + type: 'codespan', + raw: cap[0], + text, + }; + } + } + br(src) { + const cap = this.rules.inline.br.exec(src); + if (cap) { + return { + type: 'br', + raw: cap[0], + }; + } + } + del(src) { + const cap = this.rules.inline.del.exec(src); + if (cap) { + return { + type: 'del', + raw: cap[0], + text: cap[2], + tokens: this.lexer.inlineTokens(cap[2]), + }; + } + } + autolink(src) { + const cap = this.rules.inline.autolink.exec(src); + if (cap) { + let text, href; + if (cap[2] === '@') { + text = cap[1]; + href = 'mailto:' + text; + } + else { + text = cap[1]; + href = text; + } + return { + type: 'link', + raw: cap[0], + text, + href, + tokens: [ + { + type: 'text', + raw: text, + text, + }, + ], + }; + } + } + url(src) { + let cap; + if (cap = this.rules.inline.url.exec(src)) { + let text, href; + if (cap[2] === '@') { + text = cap[0]; + href = 'mailto:' + text; + } + else { + // do extended autolink path validation + let prevCapZero; + do { + prevCapZero = cap[0]; + cap[0] = this.rules.inline._backpedal.exec(cap[0])?.[0] ?? ''; + } while (prevCapZero !== cap[0]); + text = cap[0]; + if (cap[1] === 'www.') { + href = 'http://' + cap[0]; + } + else { + href = cap[0]; + } + } + return { + type: 'link', + raw: cap[0], + text, + href, + tokens: [ + { + type: 'text', + raw: text, + text, + }, + ], + }; + } + } + inlineText(src) { + const cap = this.rules.inline.text.exec(src); + if (cap) { + const escaped = this.lexer.state.inRawBlock; + return { + type: 'text', + raw: cap[0], + text: cap[0], + escaped, + }; + } + } +} + +/** + * Block Lexer + */ +class _Lexer { + tokens; + options; + state; + tokenizer; + inlineQueue; + constructor(options) { + // TokenList cannot be created in one go + this.tokens = []; + this.tokens.links = Object.create(null); + this.options = options || _defaults; + this.options.tokenizer = this.options.tokenizer || new _Tokenizer(); + this.tokenizer = this.options.tokenizer; + this.tokenizer.options = this.options; + this.tokenizer.lexer = this; + this.inlineQueue = []; + this.state = { + inLink: false, + inRawBlock: false, + top: true, + }; + const rules = { + other, + block: block.normal, + inline: inline.normal, + }; + if (this.options.pedantic) { + rules.block = block.pedantic; + rules.inline = inline.pedantic; + } + else if (this.options.gfm) { + rules.block = block.gfm; + if (this.options.breaks) { + rules.inline = inline.breaks; + } + else { + rules.inline = inline.gfm; + } + } + this.tokenizer.rules = rules; + } + /** + * Expose Rules + */ + static get rules() { + return { + block, + inline, + }; + } + /** + * Static Lex Method + */ + static lex(src, options) { + const lexer = new _Lexer(options); + return lexer.lex(src); + } + /** + * Static Lex Inline Method + */ + static lexInline(src, options) { + const lexer = new _Lexer(options); + return lexer.inlineTokens(src); + } + /** + * Preprocessing + */ + lex(src) { + src = src.replace(other.carriageReturn, '\n'); + this.blockTokens(src, this.tokens); + for (let i = 0; i < this.inlineQueue.length; i++) { + const next = this.inlineQueue[i]; + this.inlineTokens(next.src, next.tokens); + } + this.inlineQueue = []; + return this.tokens; + } + blockTokens(src, tokens = [], lastParagraphClipped = false) { + if (this.options.pedantic) { + src = src.replace(other.tabCharGlobal, ' ').replace(other.spaceLine, ''); + } + while (src) { + let token; + if (this.options.extensions?.block?.some((extTokenizer) => { + if (token = extTokenizer.call({ lexer: this }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + return false; + })) { + continue; + } + // newline + if (token = this.tokenizer.space(src)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + if (token.raw.length === 1 && lastToken !== undefined) { + // if there's a single \n as a spacer, it's terminating the last line, + // so move it there so that we don't get unnecessary paragraph tags + lastToken.raw += '\n'; + } + else { + tokens.push(token); + } + continue; + } + // code + if (token = this.tokenizer.code(src)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + // An indented code block cannot interrupt a paragraph. + if (lastToken?.type === 'paragraph' || lastToken?.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.at(-1).src = lastToken.text; + } + else { + tokens.push(token); + } + continue; + } + // fences + if (token = this.tokenizer.fences(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // heading + if (token = this.tokenizer.heading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // hr + if (token = this.tokenizer.hr(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // blockquote + if (token = this.tokenizer.blockquote(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // list + if (token = this.tokenizer.list(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // html + if (token = this.tokenizer.html(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // def + if (token = this.tokenizer.def(src)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + if (lastToken?.type === 'paragraph' || lastToken?.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.raw; + this.inlineQueue.at(-1).src = lastToken.text; + } + else if (!this.tokens.links[token.tag]) { + this.tokens.links[token.tag] = { + href: token.href, + title: token.title, + }; + } + continue; + } + // table (gfm) + if (token = this.tokenizer.table(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // lheading + if (token = this.tokenizer.lheading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // top-level paragraph + // prevent paragraph consuming extensions by clipping 'src' to extension start + let cutSrc = src; + if (this.options.extensions?.startBlock) { + let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; + this.options.extensions.startBlock.forEach((getStartIndex) => { + tempStart = getStartIndex.call({ lexer: this }, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { + startIndex = Math.min(startIndex, tempStart); + } + }); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + } + if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) { + const lastToken = tokens.at(-1); + if (lastParagraphClipped && lastToken?.type === 'paragraph') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue.at(-1).src = lastToken.text; + } + else { + tokens.push(token); + } + lastParagraphClipped = cutSrc.length !== src.length; + src = src.substring(token.raw.length); + continue; + } + // text + if (token = this.tokenizer.text(src)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + if (lastToken?.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue.at(-1).src = lastToken.text; + } + else { + tokens.push(token); + } + continue; + } + if (src) { + const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + if (this.options.silent) { + console.error(errMsg); + break; + } + else { + throw new Error(errMsg); + } + } + } + this.state.top = true; + return tokens; + } + inline(src, tokens = []) { + this.inlineQueue.push({ src, tokens }); + return tokens; + } + /** + * Lexing/Compiling + */ + inlineTokens(src, tokens = []) { + // String with links masked to avoid interference with em and strong + let maskedSrc = src; + let match = null; + // Mask out reflinks + if (this.tokens.links) { + const links = Object.keys(this.tokens.links); + if (links.length > 0) { + while ((match = this.tokenizer.rules.inline.reflinkSearch.exec(maskedSrc)) != null) { + if (links.includes(match[0].slice(match[0].lastIndexOf('[') + 1, -1))) { + maskedSrc = maskedSrc.slice(0, match.index) + + '[' + 'a'.repeat(match[0].length - 2) + ']' + + maskedSrc.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex); + } + } + } + } + // Mask out other blocks + while ((match = this.tokenizer.rules.inline.blockSkip.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex); + } + // Mask out escaped characters + while ((match = this.tokenizer.rules.inline.anyPunctuation.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '++' + maskedSrc.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex); + } + let keepPrevChar = false; + let prevChar = ''; + while (src) { + if (!keepPrevChar) { + prevChar = ''; + } + keepPrevChar = false; + let token; + // extensions + if (this.options.extensions?.inline?.some((extTokenizer) => { + if (token = extTokenizer.call({ lexer: this }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + return false; + })) { + continue; + } + // escape + if (token = this.tokenizer.escape(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // tag + if (token = this.tokenizer.tag(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // link + if (token = this.tokenizer.link(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // reflink, nolink + if (token = this.tokenizer.reflink(src, this.tokens.links)) { + src = src.substring(token.raw.length); + const lastToken = tokens.at(-1); + if (token.type === 'text' && lastToken?.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + // em & strong + if (token = this.tokenizer.emStrong(src, maskedSrc, prevChar)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // code + if (token = this.tokenizer.codespan(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // br + if (token = this.tokenizer.br(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // del (gfm) + if (token = this.tokenizer.del(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // autolink + if (token = this.tokenizer.autolink(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // url (gfm) + if (!this.state.inLink && (token = this.tokenizer.url(src))) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // text + // prevent inlineText consuming extensions by clipping 'src' to extension start + let cutSrc = src; + if (this.options.extensions?.startInline) { + let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; + this.options.extensions.startInline.forEach((getStartIndex) => { + tempStart = getStartIndex.call({ lexer: this }, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { + startIndex = Math.min(startIndex, tempStart); + } + }); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + } + if (token = this.tokenizer.inlineText(cutSrc)) { + src = src.substring(token.raw.length); + if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started + prevChar = token.raw.slice(-1); + } + keepPrevChar = true; + const lastToken = tokens.at(-1); + if (lastToken?.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + if (src) { + const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + if (this.options.silent) { + console.error(errMsg); + break; + } + else { + throw new Error(errMsg); + } + } + } + return tokens; + } +} + +/** + * Renderer + */ +class _Renderer { + options; + parser; // set by the parser + constructor(options) { + this.options = options || _defaults; + } + space(token) { + return ''; + } + code({ text, lang, escaped }) { + const langString = (lang || '').match(other.notSpaceStart)?.[0]; + const code = text.replace(other.endingNewline, '') + '\n'; + if (!langString) { + return '
    '
    +                + (escaped ? code : escape(code, true))
    +                + '
    \n'; + } + return '
    '
    +            + (escaped ? code : escape(code, true))
    +            + '
    \n'; + } + blockquote({ tokens }) { + const body = this.parser.parse(tokens); + return `
    \n${body}
    \n`; + } + html({ text }) { + return text; + } + heading({ tokens, depth }) { + return `${this.parser.parseInline(tokens)}\n`; + } + hr(token) { + return '
    \n'; + } + list(token) { + const ordered = token.ordered; + const start = token.start; + let body = ''; + for (let j = 0; j < token.items.length; j++) { + const item = token.items[j]; + body += this.listitem(item); + } + const type = ordered ? 'ol' : 'ul'; + const startAttr = (ordered && start !== 1) ? (' start="' + start + '"') : ''; + return '<' + type + startAttr + '>\n' + body + '\n'; + } + listitem(item) { + let itemBody = ''; + if (item.task) { + const checkbox = this.checkbox({ checked: !!item.checked }); + if (item.loose) { + if (item.tokens[0]?.type === 'paragraph') { + item.tokens[0].text = checkbox + ' ' + item.tokens[0].text; + if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') { + item.tokens[0].tokens[0].text = checkbox + ' ' + escape(item.tokens[0].tokens[0].text); + item.tokens[0].tokens[0].escaped = true; + } + } + else { + item.tokens.unshift({ + type: 'text', + raw: checkbox + ' ', + text: checkbox + ' ', + escaped: true, + }); + } + } + else { + itemBody += checkbox + ' '; + } + } + itemBody += this.parser.parse(item.tokens, !!item.loose); + return `
  • ${itemBody}
  • \n`; + } + checkbox({ checked }) { + return ''; + } + paragraph({ tokens }) { + return `

    ${this.parser.parseInline(tokens)}

    \n`; + } + table(token) { + let header = ''; + // header + let cell = ''; + for (let j = 0; j < token.header.length; j++) { + cell += this.tablecell(token.header[j]); + } + header += this.tablerow({ text: cell }); + let body = ''; + for (let j = 0; j < token.rows.length; j++) { + const row = token.rows[j]; + cell = ''; + for (let k = 0; k < row.length; k++) { + cell += this.tablecell(row[k]); + } + body += this.tablerow({ text: cell }); + } + if (body) + body = `${body}`; + return '\n' + + '\n' + + header + + '\n' + + body + + '
    \n'; + } + tablerow({ text }) { + return `\n${text}\n`; + } + tablecell(token) { + const content = this.parser.parseInline(token.tokens); + const type = token.header ? 'th' : 'td'; + const tag = token.align + ? `<${type} align="${token.align}">` + : `<${type}>`; + return tag + content + `\n`; + } + /** + * span level renderer + */ + strong({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + em({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + codespan({ text }) { + return `${escape(text, true)}`; + } + br(token) { + return '
    '; + } + del({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + link({ href, title, tokens }) { + const text = this.parser.parseInline(tokens); + const cleanHref = cleanUrl(href); + if (cleanHref === null) { + return text; + } + href = cleanHref; + let out = '
    '; + return out; + } + image({ href, title, text }) { + const cleanHref = cleanUrl(href); + if (cleanHref === null) { + return escape(text); + } + href = cleanHref; + let out = `${text} { + const tokens = genericToken[childTokens].flat(Infinity); + values = values.concat(this.walkTokens(tokens, callback)); + }); + } + else if (genericToken.tokens) { + values = values.concat(this.walkTokens(genericToken.tokens, callback)); + } + } + } + } + return values; + } + use(...args) { + const extensions = this.defaults.extensions || { renderers: {}, childTokens: {} }; + args.forEach((pack) => { + // copy options to new object + const opts = { ...pack }; + // set async to true if it was set to true before + opts.async = this.defaults.async || opts.async || false; + // ==-- Parse "addon" extensions --== // + if (pack.extensions) { + pack.extensions.forEach((ext) => { + if (!ext.name) { + throw new Error('extension name required'); + } + if ('renderer' in ext) { // Renderer extensions + const prevRenderer = extensions.renderers[ext.name]; + if (prevRenderer) { + // Replace extension with func to run new extension but fall back if false + extensions.renderers[ext.name] = function (...args) { + let ret = ext.renderer.apply(this, args); + if (ret === false) { + ret = prevRenderer.apply(this, args); + } + return ret; + }; + } + else { + extensions.renderers[ext.name] = ext.renderer; + } + } + if ('tokenizer' in ext) { // Tokenizer Extensions + if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) { + throw new Error("extension level must be 'block' or 'inline'"); + } + const extLevel = extensions[ext.level]; + if (extLevel) { + extLevel.unshift(ext.tokenizer); + } + else { + extensions[ext.level] = [ext.tokenizer]; + } + if (ext.start) { // Function to check for start of token + if (ext.level === 'block') { + if (extensions.startBlock) { + extensions.startBlock.push(ext.start); + } + else { + extensions.startBlock = [ext.start]; + } + } + else if (ext.level === 'inline') { + if (extensions.startInline) { + extensions.startInline.push(ext.start); + } + else { + extensions.startInline = [ext.start]; + } + } + } + } + if ('childTokens' in ext && ext.childTokens) { // Child tokens to be visited by walkTokens + extensions.childTokens[ext.name] = ext.childTokens; + } + }); + opts.extensions = extensions; + } + // ==-- Parse "overwrite" extensions --== // + if (pack.renderer) { + const renderer = this.defaults.renderer || new _Renderer(this.defaults); + for (const prop in pack.renderer) { + if (!(prop in renderer)) { + throw new Error(`renderer '${prop}' does not exist`); + } + if (['options', 'parser'].includes(prop)) { + // ignore options property + continue; + } + const rendererProp = prop; + const rendererFunc = pack.renderer[rendererProp]; + const prevRenderer = renderer[rendererProp]; + // Replace renderer with func to run extension, but fall back if false + renderer[rendererProp] = (...args) => { + let ret = rendererFunc.apply(renderer, args); + if (ret === false) { + ret = prevRenderer.apply(renderer, args); + } + return ret || ''; + }; + } + opts.renderer = renderer; + } + if (pack.tokenizer) { + const tokenizer = this.defaults.tokenizer || new _Tokenizer(this.defaults); + for (const prop in pack.tokenizer) { + if (!(prop in tokenizer)) { + throw new Error(`tokenizer '${prop}' does not exist`); + } + if (['options', 'rules', 'lexer'].includes(prop)) { + // ignore options, rules, and lexer properties + continue; + } + const tokenizerProp = prop; + const tokenizerFunc = pack.tokenizer[tokenizerProp]; + const prevTokenizer = tokenizer[tokenizerProp]; + // Replace tokenizer with func to run extension, but fall back if false + // @ts-expect-error cannot type tokenizer function dynamically + tokenizer[tokenizerProp] = (...args) => { + let ret = tokenizerFunc.apply(tokenizer, args); + if (ret === false) { + ret = prevTokenizer.apply(tokenizer, args); + } + return ret; + }; + } + opts.tokenizer = tokenizer; + } + // ==-- Parse Hooks extensions --== // + if (pack.hooks) { + const hooks = this.defaults.hooks || new _Hooks(); + for (const prop in pack.hooks) { + if (!(prop in hooks)) { + throw new Error(`hook '${prop}' does not exist`); + } + if (['options', 'block'].includes(prop)) { + // ignore options and block properties + continue; + } + const hooksProp = prop; + const hooksFunc = pack.hooks[hooksProp]; + const prevHook = hooks[hooksProp]; + if (_Hooks.passThroughHooks.has(prop)) { + // @ts-expect-error cannot type hook function dynamically + hooks[hooksProp] = (arg) => { + if (this.defaults.async) { + return Promise.resolve(hooksFunc.call(hooks, arg)).then(ret => { + return prevHook.call(hooks, ret); + }); + } + const ret = hooksFunc.call(hooks, arg); + return prevHook.call(hooks, ret); + }; + } + else { + // @ts-expect-error cannot type hook function dynamically + hooks[hooksProp] = (...args) => { + let ret = hooksFunc.apply(hooks, args); + if (ret === false) { + ret = prevHook.apply(hooks, args); + } + return ret; + }; + } + } + opts.hooks = hooks; + } + // ==-- Parse WalkTokens extensions --== // + if (pack.walkTokens) { + const walkTokens = this.defaults.walkTokens; + const packWalktokens = pack.walkTokens; + opts.walkTokens = function (token) { + let values = []; + values.push(packWalktokens.call(this, token)); + if (walkTokens) { + values = values.concat(walkTokens.call(this, token)); + } + return values; + }; + } + this.defaults = { ...this.defaults, ...opts }; + }); + return this; + } + setOptions(opt) { + this.defaults = { ...this.defaults, ...opt }; + return this; + } + lexer(src, options) { + return _Lexer.lex(src, options ?? this.defaults); + } + parser(tokens, options) { + return _Parser.parse(tokens, options ?? this.defaults); + } + parseMarkdown(blockType) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const parse = (src, options) => { + const origOpt = { ...options }; + const opt = { ...this.defaults, ...origOpt }; + const throwError = this.onError(!!opt.silent, !!opt.async); + // throw error if an extension set async to true but parse was called with async: false + if (this.defaults.async === true && origOpt.async === false) { + return throwError(new Error('marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise.')); + } + // throw error in case of non string input + if (typeof src === 'undefined' || src === null) { + return throwError(new Error('marked(): input parameter is undefined or null')); + } + if (typeof src !== 'string') { + return throwError(new Error('marked(): input parameter is of type ' + + Object.prototype.toString.call(src) + ', string expected')); + } + if (opt.hooks) { + opt.hooks.options = opt; + opt.hooks.block = blockType; + } + const lexer = opt.hooks ? opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline); + const parser = opt.hooks ? opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline); + if (opt.async) { + return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) + .then(src => lexer(src, opt)) + .then(tokens => opt.hooks ? opt.hooks.processAllTokens(tokens) : tokens) + .then(tokens => opt.walkTokens ? Promise.all(this.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens) + .then(tokens => parser(tokens, opt)) + .then(html => opt.hooks ? opt.hooks.postprocess(html) : html) + .catch(throwError); + } + try { + if (opt.hooks) { + src = opt.hooks.preprocess(src); + } + let tokens = lexer(src, opt); + if (opt.hooks) { + tokens = opt.hooks.processAllTokens(tokens); + } + if (opt.walkTokens) { + this.walkTokens(tokens, opt.walkTokens); + } + let html = parser(tokens, opt); + if (opt.hooks) { + html = opt.hooks.postprocess(html); + } + return html; + } + catch (e) { + return throwError(e); + } + }; + return parse; + } + onError(silent, async) { + return (e) => { + e.message += '\nPlease report this to https://github.com/markedjs/marked.'; + if (silent) { + const msg = '

    An error occurred:

    '
    +                    + escape(e.message + '', true)
    +                    + '
    '; + if (async) { + return Promise.resolve(msg); + } + return msg; + } + if (async) { + return Promise.reject(e); + } + throw e; + }; + } +} + +const markedInstance = new Marked(); +function marked(src, opt) { + return markedInstance.parse(src, opt); +} +/** + * Sets the default options. + * + * @param options Hash of options + */ +marked.options = + marked.setOptions = function (options) { + markedInstance.setOptions(options); + marked.defaults = markedInstance.defaults; + changeDefaults(marked.defaults); + return marked; + }; +/** + * Gets the original marked default options. + */ +marked.getDefaults = _getDefaults; +marked.defaults = _defaults; +/** + * Use Extension + */ +marked.use = function (...args) { + markedInstance.use(...args); + marked.defaults = markedInstance.defaults; + changeDefaults(marked.defaults); + return marked; +}; +/** + * Run callback for every token + */ +marked.walkTokens = function (tokens, callback) { + return markedInstance.walkTokens(tokens, callback); +}; +/** + * Compiles markdown to HTML without enclosing `p` tag. + * + * @param src String of markdown source to be compiled + * @param options Hash of options + * @return String of compiled HTML + */ +marked.parseInline = markedInstance.parseInline; +/** + * Expose + */ +marked.Parser = _Parser; +marked.parser = _Parser.parse; +marked.Renderer = _Renderer; +marked.TextRenderer = _TextRenderer; +marked.Lexer = _Lexer; +marked.lexer = _Lexer.lex; +marked.Tokenizer = _Tokenizer; +marked.Hooks = _Hooks; +marked.parse = marked; +const options = marked.options; +const setOptions = marked.setOptions; +const use = marked.use; +const walkTokens = marked.walkTokens; +const parseInline = marked.parseInline; +const parse = marked; +const parser = _Parser.parse; +const lexer = _Lexer.lex; + +export { _Hooks as Hooks, _Lexer as Lexer, Marked, _Parser as Parser, _Renderer as Renderer, _TextRenderer as TextRenderer, _Tokenizer as Tokenizer, _defaults as defaults, _getDefaults as getDefaults, lexer, marked, options, parse, parseInline, parser, setOptions, use, walkTokens }; +//# sourceMappingURL=marked.esm.js.map diff --git a/third_party/marked/src/marked.min.js b/third_party/marked/src/marked.min.js new file mode 100644 index 00000000000000..d21d5e98a9222b --- /dev/null +++ b/third_party/marked/src/marked.min.js @@ -0,0 +1,11 @@ +/** + * Copy it as-is, but ensure to give proper credit to third-party/marked, + * following the recommended practices outlined by Chromium. + */ + +/** + * marked v15.0.3 - a markdown parser + * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s={exec:()=>null};function r(e,t=""){let n="string"==typeof e?e:e.source;const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(i.caret,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}const i={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^
    /i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:e=>new RegExp(`^( {0,3}${e})((?:[\t ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),hrRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}#`),htmlBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}<(?:[a-z].*>|!--)`,"i")},l=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,o=/(?:[*+-]|\d{1,9}[.)])/,a=r(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,o).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),c=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,h=/(?!\s*\])(?:\\.|[^\[\]\\])+/,p=r(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",h).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),u=r(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,o).getRegex(),g="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",k=/|$))/,d=r("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$))","i").replace("comment",k).replace("tag",g).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),f=r(c).replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",g).getRegex(),x={blockquote:r(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",f).getRegex(),code:/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,def:p,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:l,html:d,lheading:a,list:u,newline:/^(?:[ \t]*(?:\n|$))+/,paragraph:f,table:s,text:/^[^\n]+/},b=r("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3}\t)[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",g).getRegex(),w={...x,table:b,paragraph:r(c).replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",b).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",g).getRegex()},m={...x,html:r("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",k).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:s,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:r(c).replace("hr",l).replace("heading"," *#{1,6} *[^\n]").replace("lheading",a).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},y=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,$=/^( {2,}|\\)\n(?!\s*$)/,R=/[\p{P}\p{S}]/u,S=/[\s\p{P}\p{S}]/u,T=/[^\s\p{P}\p{S}]/u,z=r(/^((?![*_])punctSpace)/,"u").replace(/punctSpace/g,S).getRegex(),A=r(/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,"u").replace(/punct/g,R).getRegex(),_=r("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)","gu").replace(/notPunctSpace/g,T).replace(/punctSpace/g,S).replace(/punct/g,R).getRegex(),P=r("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,T).replace(/punctSpace/g,S).replace(/punct/g,R).getRegex(),I=r(/\\(punct)/,"gu").replace(/punct/g,R).getRegex(),L=r(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),B=r(k).replace("(?:--\x3e|$)","--\x3e").getRegex(),C=r("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",B).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),E=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,q=r(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",E).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),Z=r(/^!?\[(label)\]\[(ref)\]/).replace("label",E).replace("ref",h).getRegex(),v=r(/^!?\[(ref)\](?:\[\])?/).replace("ref",h).getRegex(),D={_backpedal:s,anyPunctuation:I,autolink:L,blockSkip:/\[[^[\]]*?\]\((?:\\.|[^\\\(\)]|\((?:\\.|[^\\\(\)])*\))*\)|`[^`]*?`|<[^<>]*?>/g,br:$,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:s,emStrongLDelim:A,emStrongRDelimAst:_,emStrongRDelimUnd:P,escape:y,link:q,nolink:v,punctuation:z,reflink:Z,reflinkSearch:r("reflink|nolink(?!\\()","g").replace("reflink",Z).replace("nolink",v).getRegex(),tag:C,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},H=e=>G[e];function X(e,t){if(t){if(i.escapeTest.test(e))return e.replace(i.escapeReplace,H)}else if(i.escapeTestNoEncode.test(e))return e.replace(i.escapeReplaceNoEncode,H);return e}function F(e){try{e=encodeURI(e).replace(i.percentDecode,"%")}catch{return null}return e}function U(e,t){const n=e.replace(i.findPipe,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(i.splitPipe);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:J(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t,n){const s=e.match(n.other.indentCodeCompensation);if(null===s)return t;const r=s[1];return t.split("\n").map((e=>{const t=e.match(n.other.beginningSpace);if(null===t)return e;const[s]=t;return s.length>=r.length?e.slice(r.length):e})).join("\n")}(e,t[3]||"",this.rules);return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(this.rules.other.endingHash.test(e)){const t=J(e,"#");this.options.pedantic?e=t.trim():t&&!this.rules.other.endingSpaceChar.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:J(t[0],"\n")}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=J(t[0],"\n").split("\n"),n="",s="";const r=[];for(;e.length>0;){let t=!1;const i=[];let l;for(l=0;l1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=this.rules.other.listItemRegex(n);let l=!1;for(;e;){let n=!1,s="",o="";if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;s=t[0],e=e.substring(s.length);let a=t[2].split("\n",1)[0].replace(this.rules.other.listReplaceTabs,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=!a.trim(),p=0;if(this.options.pedantic?(p=2,o=a.trimStart()):h?p=t[1].length+1:(p=t[2].search(this.rules.other.nonSpaceChar),p=p>4?1:p,o=a.slice(p),p+=t[1].length),h&&this.rules.other.blankLine.test(c)&&(s+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=this.rules.other.nextBulletRegex(p),n=this.rules.other.hrRegex(p),r=this.rules.other.fencesBeginRegex(p),i=this.rules.other.headingBeginRegex(p),l=this.rules.other.htmlBeginRegex(p);for(;e;){const u=e.split("\n",1)[0];let g;if(c=u,this.options.pedantic?(c=c.replace(this.rules.other.listReplaceNesting," "),g=c):g=c.replace(this.rules.other.tabCharGlobal," "),r.test(c))break;if(i.test(c))break;if(l.test(c))break;if(t.test(c))break;if(n.test(c))break;if(g.search(this.rules.other.nonSpaceChar)>=p||!c.trim())o+="\n"+g.slice(p);else{if(h)break;if(a.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4)break;if(r.test(a))break;if(i.test(a))break;if(n.test(a))break;o+="\n"+c}h||c.trim()||(h=!0),s+=u+"\n",e=e.substring(u.length+1),a=g.slice(p)}}r.loose||(l?r.loose=!0:this.rules.other.doubleBlankLine.test(s)&&(l=!0));let u,g=null;this.options.gfm&&(g=this.rules.other.listIsTask.exec(o),g&&(u="[ ] "!==g[0],o=o.replace(this.rules.other.listReplaceTask,""))),r.items.push({type:"list_item",raw:s,task:!!g,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=s}const o=r.items.at(-1);o&&(o.raw=o.raw.trimEnd(),o.text=o.text.trimEnd()),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>this.rules.other.anyLine.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e({text:e,tokens:this.lexer.inline(e),header:!1,align:i.align[t]}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(e)){if(!this.rules.other.endAngleBracket.test(e))return;const t=J(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=this.rules.other.pedanticHrefTitle.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),this.rules.other.startAngleBracket.test(n)&&(n=this.options.pedantic&&!this.rules.other.endAngleBracket.test(e)?n.slice(1):n.slice(1,-1)),K(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return K(n,e,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(this.rules.other.newLineCharGlobal," ");const n=this.rules.other.nonSpaceChar.test(e),s=this.rules.other.startingSpaceChar.test(e)&&this.rules.other.endingSpaceChar.test(e);return n&&s&&(e=e.substring(1,e.length-1)),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=t[1],n="mailto:"+e):(e=t[1],n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=t[0],n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=t[0],n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){const e=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:e}}}}class W{tokens;options;state;tokenizer;inlineQueue;constructor(t){this.tokens=[],this.tokens.links=Object.create(null),this.options=t||e.defaults,this.options.tokenizer=this.options.tokenizer||new V,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};const n={other:i,block:j.normal,inline:N.normal};this.options.pedantic?(n.block=j.pedantic,n.inline=N.pedantic):this.options.gfm&&(n.block=j.gfm,this.options.breaks?n.inline=N.breaks:n.inline=N.gfm),this.tokenizer.rules=n}static get rules(){return{block:j,inline:N}}static lex(e,t){return new W(t).lex(e)}static lexInline(e,t){return new W(t).inlineTokens(e)}lex(e){e=e.replace(i.carriageReturn,"\n"),this.blockTokens(e,this.tokens);for(let e=0;e!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0))))continue;if(s=this.tokenizer.space(e)){e=e.substring(s.raw.length);const n=t.at(-1);1===s.raw.length&&void 0!==n?n.raw+="\n":t.push(s);continue}if(s=this.tokenizer.code(e)){e=e.substring(s.raw.length);const n=t.at(-1);"paragraph"===n?.type||"text"===n?.type?(n.raw+="\n"+s.raw,n.text+="\n"+s.text,this.inlineQueue.at(-1).src=n.text):t.push(s);continue}if(s=this.tokenizer.fences(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.heading(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.hr(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.blockquote(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.list(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.html(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.def(e)){e=e.substring(s.raw.length);const n=t.at(-1);"paragraph"===n?.type||"text"===n?.type?(n.raw+="\n"+s.raw,n.text+="\n"+s.raw,this.inlineQueue.at(-1).src=n.text):this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title});continue}if(s=this.tokenizer.table(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.lheading(e)){e=e.substring(s.raw.length),t.push(s);continue}let r=e;if(this.options.extensions?.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(s=this.tokenizer.paragraph(r))){const i=t.at(-1);n&&"paragraph"===i?.type?(i.raw+="\n"+s.raw,i.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=i.text):t.push(s),n=r.length!==e.length,e=e.substring(s.raw.length)}else if(s=this.tokenizer.text(e)){e=e.substring(s.raw.length);const n=t.at(-1);"text"===n?.type?(n.raw+="\n"+s.raw,n.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=n.text):t.push(s)}else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,s=null;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(s=this.tokenizer.rules.inline.reflinkSearch.exec(n));)e.includes(s[0].slice(s[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(s=this.tokenizer.rules.inline.blockSkip.exec(n));)n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(s=this.tokenizer.rules.inline.anyPunctuation.exec(n));)n=n.slice(0,s.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);let r=!1,i="";for(;e;){let s;if(r||(i=""),r=!1,this.options.extensions?.inline?.some((n=>!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0))))continue;if(s=this.tokenizer.escape(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.tag(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.link(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(s.raw.length);const n=t.at(-1);"text"===s.type&&"text"===n?.type?(n.raw+=s.raw,n.text+=s.text):t.push(s);continue}if(s=this.tokenizer.emStrong(e,n,i)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.codespan(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.br(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.del(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.autolink(e)){e=e.substring(s.raw.length),t.push(s);continue}if(!this.state.inLink&&(s=this.tokenizer.url(e))){e=e.substring(s.raw.length),t.push(s);continue}let l=e;if(this.options.extensions?.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(l=e.substring(0,t+1))}if(s=this.tokenizer.inlineText(l)){e=e.substring(s.raw.length),"_"!==s.raw.slice(-1)&&(i=s.raw.slice(-1)),r=!0;const n=t.at(-1);"text"===n?.type?(n.raw+=s.raw,n.text+=s.text):t.push(s)}else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return t}}class Y{options;parser;constructor(t){this.options=t||e.defaults}space(e){return""}code({text:e,lang:t,escaped:n}){const s=(t||"").match(i.notSpaceStart)?.[0],r=e.replace(i.endingNewline,"")+"\n";return s?'
    '+(n?r:X(r,!0))+"
    \n":"
    "+(n?r:X(r,!0))+"
    \n"}blockquote({tokens:e}){return`
    \n${this.parser.parse(e)}
    \n`}html({text:e}){return e}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)}\n`}hr(e){return"
    \n"}list(e){const t=e.ordered,n=e.start;let s="";for(let t=0;t\n"+s+"\n"}listitem(e){let t="";if(e.task){const n=this.checkbox({checked:!!e.checked});e.loose?"paragraph"===e.tokens[0]?.type?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&"text"===e.tokens[0].tokens[0].type&&(e.tokens[0].tokens[0].text=n+" "+X(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • \n`}checkbox({checked:e}){return"'}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    \n`}table(e){let t="",n="";for(let t=0;t${s}`),"\n\n"+t+"\n"+s+"
    \n"}tablerow({text:e}){return`\n${e}\n`}tablecell(e){const t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`\n`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${X(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){const s=this.parser.parseInline(n),r=F(e);if(null===r)return s;let i='
    ",i}image({href:e,title:t,text:n}){const s=F(e);if(null===s)return X(n);let r=`${n}{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new Y(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if(["options","parser"].includes(n))continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new V(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new ne;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if(["options","block"].includes(n))continue;const s=n,r=e.hooks[s],i=t[s];ne.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return W.lex(e,t??this.defaults)}parser(e,t){return te.parse(e,t??this.defaults)}parseMarkdown(e){return(t,n)=>{const s={...n},r={...this.defaults,...s},i=this.onError(!!r.silent,!!r.async);if(!0===this.defaults.async&&!1===s.async)return i(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(null==t)return i(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof t)return i(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(t)+", string expected"));r.hooks&&(r.hooks.options=r,r.hooks.block=e);const l=r.hooks?r.hooks.provideLexer():e?W.lex:W.lexInline,o=r.hooks?r.hooks.provideParser():e?te.parse:te.parseInline;if(r.async)return Promise.resolve(r.hooks?r.hooks.preprocess(t):t).then((e=>l(e,r))).then((e=>r.hooks?r.hooks.processAllTokens(e):e)).then((e=>r.walkTokens?Promise.all(this.walkTokens(e,r.walkTokens)).then((()=>e)):e)).then((e=>o(e,r))).then((e=>r.hooks?r.hooks.postprocess(e):e)).catch(i);try{r.hooks&&(t=r.hooks.preprocess(t));let e=l(t,r);r.hooks&&(e=r.hooks.processAllTokens(e)),r.walkTokens&&this.walkTokens(e,r.walkTokens);let n=o(e,r);return r.hooks&&(n=r.hooks.postprocess(n)),n}catch(e){return i(e)}}}onError(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+X(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const re=new se;function ie(e,t){return re.parse(e,t)}ie.options=ie.setOptions=function(e){return re.setOptions(e),ie.defaults=re.defaults,n(ie.defaults),ie},ie.getDefaults=t,ie.defaults=e.defaults,ie.use=function(...e){return re.use(...e),ie.defaults=re.defaults,n(ie.defaults),ie},ie.walkTokens=function(e,t){return re.walkTokens(e,t)},ie.parseInline=re.parseInline,ie.Parser=te,ie.parser=te.parse,ie.Renderer=Y,ie.TextRenderer=ee,ie.Lexer=W,ie.lexer=W.lex,ie.Tokenizer=V,ie.Hooks=ne,ie.parse=ie;const le=ie.options,oe=ie.setOptions,ae=ie.use,ce=ie.walkTokens,he=ie.parseInline,pe=ie,ue=te.parse,ge=W.lex;e.Hooks=ne,e.Lexer=W,e.Marked=se,e.Parser=te,e.Renderer=Y,e.TextRenderer=ee,e.Tokenizer=V,e.getDefaults=t,e.lexer=ge,e.marked=ie,e.options=le,e.parse=pe,e.parseInline=he,e.parser=ue,e.setOptions=oe,e.use=ae,e.walkTokens=ce})); diff --git a/third_party/node/node_modules.tar.gz.sha1 b/third_party/node/node_modules.tar.gz.sha1 index e08609e4197f39..5edab10ac734a3 100644 --- a/third_party/node/node_modules.tar.gz.sha1 +++ b/third_party/node/node_modules.tar.gz.sha1 @@ -1 +1 @@ -2f09ef382b6bc57db929ed7632af0ef86f7e036f +023cf304f3e5767bf16756e7f78bd23444001dcd diff --git a/third_party/node/npm_exclude.txt b/third_party/node/npm_exclude.txt index 13c16d04e8af43..696ccafb0ef2ce 100644 --- a/third_party/node/npm_exclude.txt +++ b/third_party/node/npm_exclude.txt @@ -49,3 +49,12 @@ lit-html/* # Exclude unnecessary files from @typescript-eslint. @typescript-eslint/eslint-plugin/docs/* + +# Exclude marked js +marked/bin/* +marked/lib/marked.cjs +marked/lib/marked.cts +marked/lib/marked.d.cts +marked/lib/marked.umd.js +marked/man/* +marked/README.md diff --git a/third_party/node/npm_include.txt b/third_party/node/npm_include.txt index 9c113407cd07aa..4ab42d2704836a 100644 --- a/third_party/node/npm_include.txt +++ b/third_party/node/npm_include.txt @@ -41,6 +41,7 @@ @types/wicg-file-system-access/index.d.ts @typescript-eslint/types/dist/**.d.ts @typescript-eslint/utils/dist/**.d.ts +@types/marked/* /chai/chai.js /chai/LICENSE /chai/package.json @@ -53,3 +54,8 @@ /typescript/lib/tsc.js /typescript/lib/tsserver.js /typescript/lib/typescript.js +/marked/lib/marked.esm.js +/marked/marked.min.js +/marked/lib/marked.d.ts +/marked/LICENSE.md +/marked/package.json \ No newline at end of file diff --git a/third_party/node/package-lock.json b/third_party/node/package-lock.json index 5f7b0480cf5336..0cc8971e92061f 100644 --- a/third_party/node/package-lock.json +++ b/third_party/node/package-lock.json @@ -21,6 +21,7 @@ "@types/filesystem": "0.0.32", "@types/filewriter": "0.0.29", "@types/google.analytics": "0.0.42", + "@types/marked": "6.0.0", "@types/mocha": "10.0.6", "@types/offscreencanvas": "2019.6.4", "@types/trusted-types": "1.0.6", @@ -35,6 +36,7 @@ "eslint-plugin-jsdoc": "50.2.2", "html-minifier": "4.0.0", "lit": "3.0.2", + "marked": "^15.0.3", "svgo": "3.0.2", "terser": "5.31.6", "typescript": "5.5.3" @@ -643,6 +645,15 @@ "resolved": "https://registry.npmjs.org/@types/google.analytics/-/google.analytics-0.0.42.tgz", "integrity": "sha512-w0ZFj3SHznQXSq99kFCuO8tkN6w4T14znjrF2alLCSDnHOXEnpzneyNwxLvekcsDBInr8b5mXmzYh03GArqEyw==" }, + "node_modules/@types/marked": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-6.0.0.tgz", + "integrity": "sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA==", + "deprecated": "This is a stub types definition. marked provides its own type definitions, so you do not need this installed.", + "dependencies": { + "marked": "*" + } + }, "node_modules/@types/mocha": { "version": "10.0.6", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", @@ -1797,6 +1808,17 @@ "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==" }, + "node_modules/marked": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.3.tgz", + "integrity": "sha512-Ai0cepvl2NHnTcO9jYDtcOEtVBNVYR31XnEA3BndO7f5As1wzpcOceSUM8FDkNLJNIODcLpDTWay/qQhqbuMvg==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -2820,6 +2842,14 @@ "resolved": "https://registry.npmjs.org/@types/google.analytics/-/google.analytics-0.0.42.tgz", "integrity": "sha512-w0ZFj3SHznQXSq99kFCuO8tkN6w4T14znjrF2alLCSDnHOXEnpzneyNwxLvekcsDBInr8b5mXmzYh03GArqEyw==" }, + "@types/marked": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-6.0.0.tgz", + "integrity": "sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA==", + "requires": { + "marked": "*" + } + }, "@types/mocha": { "version": "10.0.6", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", @@ -3633,6 +3663,11 @@ "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==" }, + "marked": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.3.tgz", + "integrity": "sha512-Ai0cepvl2NHnTcO9jYDtcOEtVBNVYR31XnEA3BndO7f5As1wzpcOceSUM8FDkNLJNIODcLpDTWay/qQhqbuMvg==" + }, "mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", diff --git a/third_party/node/package.json b/third_party/node/package.json index fe18cf83718700..661b0e0d50bd6b 100644 --- a/third_party/node/package.json +++ b/third_party/node/package.json @@ -26,12 +26,14 @@ "@typescript-eslint/parser": "8.4.0", "@typescript-eslint/types": "8.4.0", "@typescript-eslint/utils": "8.4.0", + "@types/marked": "6.0.0", "eslint": "9.10.0", "eslint-plugin-jsdoc": "50.2.2", "html-minifier": "4.0.0", "lit": "3.0.2", "svgo": "3.0.2", "terser": "5.31.6", - "typescript": "5.5.3" + "typescript": "5.5.3", + "marked": "^15.0.3" } } diff --git a/tools/gritsettings/resource_ids.spec b/tools/gritsettings/resource_ids.spec index 38cf9d8fd803f4..33c794971e4c9a 100644 --- a/tools/gritsettings/resource_ids.spec +++ b/tools/gritsettings/resource_ids.spec @@ -588,6 +588,10 @@ "META": {"sizes": {"includes": [15],}}, "includes": [4900], }, + "<(SHARED_INTERMEDIATE_DIR)/chrome/browser/resources/side_panel/chat/resources.grd": { + "META": {"sizes": {"includes": [20]}}, + "includes": [4910], + }, "<(SHARED_INTERMEDIATE_DIR)/chrome/browser/resources/side_panel/shared/resources.grd": { "META": {"sizes": {"includes": [15],}}, "includes": [4920], diff --git a/tools/metrics/histograms/metadata/others/histograms.xml b/tools/metrics/histograms/metadata/others/histograms.xml index 279040555faff1..2b7bc34d8e5b0c 100644 --- a/tools/metrics/histograms/metadata/others/histograms.xml +++ b/tools/metrics/histograms/metadata/others/histograms.xml @@ -206,6 +206,7 @@ chromium-metrics-reviews@google.com. + diff --git a/tools/metrics/histograms/metadata/page/histograms.xml b/tools/metrics/histograms/metadata/page/histograms.xml index a2ec77cd1cfca9..c88038a0b3ad4e 100644 --- a/tools/metrics/histograms/metadata/page/histograms.xml +++ b/tools/metrics/histograms/metadata/page/histograms.xml @@ -241,6 +241,7 @@ chromium-metrics-reviews@google.com. + diff --git a/ui/webui/resources/js/parse_html_subset.ts b/ui/webui/resources/js/parse_html_subset.ts index 504665e8cb1f8d..3ab27daa9a100c 100644 --- a/ui/webui/resources/js/parse_html_subset.ts +++ b/ui/webui/resources/js/parse_html_subset.ts @@ -44,6 +44,18 @@ export function sanitizeInnerHtml( } return sanitizedPolicy.createHTML(rawString, opts); } + +export function getTrustedHTML(rawString: string) : (TrustedHTML | string) { + assert(window.trustedTypes); + if (sanitizedPolicy === null) { + sanitizedPolicy = window.trustedTypes.createPolicy('sanitize-inner-html', { + createHTML: (s : string) => s, + createScript: () => assertNotReached(), + createScriptURL: () => assertNotReached(), + }); + } + return sanitizedPolicy.createHTML(rawString); +} // //