diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index 3b54d304979..29cbf35f91a 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -5,6 +5,7 @@ breadcrumbs ccmp ccon clickable +cmark CMMI colorbrewer consvc @@ -22,6 +23,7 @@ Emacspeak Fitt FTCS gantt +gfm ghe gje godbolt diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 364080afcd4..3ffc52f9fb2 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -43,6 +43,7 @@ DONTADDTORECENT DWMSBT DWMWA DWORDLONG +EMPH endfor ENDSESSION enumset @@ -62,6 +63,7 @@ GETDESKWALLPAPER GETHIGHCONTRAST GETMOUSEHOVERTIME GETTEXTLENGTH +HARDBREAKS Hashtable HIGHCONTRASTON HIGHCONTRASTW @@ -115,6 +117,7 @@ IUri IVirtual KEYSELECT LCID +LINEBREAK llabs llu localtime @@ -148,6 +151,7 @@ NIF NIN NOAGGREGATION NOASYNC +NOBREAKS NOCHANGEDIR NOPROGRESS NOREDIRECTIONBITMAP @@ -204,6 +208,7 @@ SINGLEUSE SIZENS smoothstep snprintf +SOFTBREAK spsc sregex SRWLOC diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt index 5da4245ffb8..186faef8aab 100644 --- a/.github/actions/spelling/excludes.txt +++ b/.github/actions/spelling/excludes.txt @@ -104,6 +104,7 @@ ^doc/reference/UTF8-torture-test\.txt$ ^doc/reference/windows-terminal-logo\.ans$ ^oss/ +^NOTICE.md ^samples/PixelShaders/Screenshots/ ^src/interactivity/onecore/BgfxEngine\. ^src/renderer/atlas/ diff --git a/NOTICE.md b/NOTICE.md index c08d041f8e2..bd99dd939ca 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -281,6 +281,181 @@ CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` +## cmark +**Source**: [https://github.com/commonmark/cmark](https://github.com/commonmark/cmark) + +### License +Copyright (c) 2014, John MacFarlane + +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. + +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. + +----- + +houdini.h, houdini_href_e.c, houdini_html_e.c, houdini_html_u.c + +derive from https://github.com/vmg/houdini (with some modifications) + +Copyright (C) 2012 Vicent Martí + +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. + +----- + +buffer.h, buffer.c, chunk.h + +are derived from code (C) 2012 Github, Inc. + +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. + +----- + +utf8.c and utf8.c + +are derived from utf8proc +(), +(C) 2009 Public Software Group e. V., Berlin, Germany. + +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. + +----- + +The normalization code in normalize.py was derived from the +markdowntest project, Copyright 2013 Karl Dubost: + +The MIT License (MIT) + +Copyright (c) 2013 Karl Dubost + +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. + +----- + +The CommonMark spec (test/spec.txt) is + +Copyright (C) 2014-15 John MacFarlane + +Released under the Creative Commons CC-BY-SA 4.0 license: +. + +----- + +The test software in test/ is + +Copyright (c) 2014, John MacFarlane + +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. + +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. + # Microsoft Open Source This product also incorporates source code from other Microsoft open source projects, all licensed under the MIT license. diff --git a/OpenConsole.sln b/OpenConsole.sln index 6d1e8aefba2..a92c45a48ac 100644 --- a/OpenConsole.sln +++ b/OpenConsole.sln @@ -405,6 +405,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RenderingTests", "src\tools EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Terminal.UI", "src\cascadia\UIHelpers\UIHelpers.vcxproj", "{6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Terminal.UI.Markdown", "src\cascadia\UIMarkdown\UIMarkdown.vcxproj", "{7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}" +EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "benchcat", "src\tools\benchcat\benchcat.vcxproj", "{2C836962-9543-4CE5-B834-D28E1F124B66}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ConsoleMonitor", "src\tools\ConsoleMonitor\ConsoleMonitor.vcxproj", "{328729E9-6723-416E-9C98-951F1473BBE1}" @@ -2302,6 +2304,28 @@ Global {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Release|x64.Build.0 = Release|x64 {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Release|x86.ActiveCfg = Release|Win32 {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Release|x86.Build.0 = Release|Win32 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.AuditMode|Any CPU.ActiveCfg = Debug|Win32 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.AuditMode|x64.ActiveCfg = Release|x64 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.AuditMode|x86.ActiveCfg = Release|Win32 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Debug|Any CPU.ActiveCfg = Debug|x64 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Debug|ARM64.Build.0 = Debug|ARM64 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Debug|x64.ActiveCfg = Debug|x64 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Debug|x64.Build.0 = Debug|x64 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Debug|x86.ActiveCfg = Debug|Win32 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Debug|x86.Build.0 = Debug|Win32 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|x64 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Fuzzing|x64.ActiveCfg = Fuzzing|x64 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Release|Any CPU.ActiveCfg = Release|x64 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Release|ARM64.ActiveCfg = Release|ARM64 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Release|ARM64.Build.0 = Release|ARM64 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Release|x64.ActiveCfg = Release|x64 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Release|x64.Build.0 = Release|x64 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Release|x86.ActiveCfg = Release|Win32 + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F}.Release|x86.Build.0 = Release|Win32 {2C836962-9543-4CE5-B834-D28E1F124B66}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32 {2C836962-9543-4CE5-B834-D28E1F124B66}.AuditMode|ARM64.ActiveCfg = Release|ARM64 {2C836962-9543-4CE5-B834-D28E1F124B66}.AuditMode|x64.ActiveCfg = Release|x64 @@ -2455,6 +2479,7 @@ Global {613CCB57-5FA9-48EF-80D0-6B1E319E20C4} = {A10C4720-DCA4-4640-9749-67F4314F527C} {37C995E0-2349-4154-8E77-4A52C0C7F46D} = {A10C4720-DCA4-4640-9749-67F4314F527C} {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F} = {61901E80-E97D-4D61-A9BB-E8F2FDA8B40C} + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F} = {61901E80-E97D-4D61-A9BB-E8F2FDA8B40C} {2C836962-9543-4CE5-B834-D28E1F124B66} = {A10C4720-DCA4-4640-9749-67F4314F527C} {328729E9-6723-416E-9C98-951F1473BBE1} = {A10C4720-DCA4-4640-9749-67F4314F527C} {BE92101C-04F8-48DA-99F0-E1F4F1D2DC48} = {A10C4720-DCA4-4640-9749-67F4314F527C} diff --git a/src/cascadia/TerminalApp/MarkdownPaneContent.cpp b/src/cascadia/TerminalApp/MarkdownPaneContent.cpp new file mode 100644 index 00000000000..f287b2c1c72 --- /dev/null +++ b/src/cascadia/TerminalApp/MarkdownPaneContent.cpp @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "MarkdownPaneContent.h" +#include +#include "MarkdownPaneContent.g.cpp" +#include + +using namespace std::chrono_literals; +using namespace winrt::Microsoft::Terminal; +using namespace winrt::Microsoft::Terminal::Settings; +using namespace winrt::Microsoft::Terminal::Settings::Model; + +namespace winrt +{ + namespace MUX = Microsoft::UI::Xaml; + namespace WUX = Windows::UI::Xaml; + using IInspectable = Windows::Foundation::IInspectable; +} + +namespace winrt::TerminalApp::implementation +{ + + MarkdownPaneContent::MarkdownPaneContent() : + MarkdownPaneContent(L"") {} + + MarkdownPaneContent::MarkdownPaneContent(const winrt::hstring& initialPath) + { + InitializeComponent(); + + FilePathInput().Text(initialPath); + _filePath = FilePathInput().Text(); + _loadFile(); + } + + INewContentArgs MarkdownPaneContent::GetNewTerminalArgs(BuildStartupKind /*kind*/) const + { + return BaseContentArgs(L"x-markdown"); + } + + void MarkdownPaneContent::_clearOldNotebook() + { + RenderedMarkdown().Children().Clear(); + } + void MarkdownPaneContent::_loadFile() + { + if (_filePath.empty()) + { + return; + } + + // Our title is the path of our MD file + TitleChanged.raise(*this, nullptr); + + const std::filesystem::path filePath{ std::wstring_view{ _filePath } }; + const auto markdownContents{ til::io::read_file_as_utf8_string_if_exists(filePath) }; + + Editing(false); + PropertyChanged.raise(*this, WUX::Data::PropertyChangedEventArgs{ L"Editing" }); + FileContents(winrt::to_hstring(markdownContents)); + PropertyChanged.raise(*this, WUX::Data::PropertyChangedEventArgs{ L"FileContents" }); + + _renderFileContents(); + } + void MarkdownPaneContent::_renderFileContents() + { + // Was the file a .md file? + if (_filePath.ends_with(L".md")) + { + _loadMarkdown(); + } + else + { + _loadText(); + } + } + void MarkdownPaneContent::_loadText() + { + auto block = WUX::Controls::TextBlock(); + block.IsTextSelectionEnabled(true); + block.FontFamily(WUX::Media::FontFamily{ L"Cascadia Code" }); + block.Text(FileContents()); + + RenderedMarkdown().Children().Append(block); + } + + void MarkdownPaneContent::_loadMarkdown() + { + auto rootTextBlock{ Microsoft::Terminal::UI::Markdown::Builder::Convert(FileContents(), _filePath) }; + + // By default, the markdown pane doesn't have play buttons next to the + // blocks. But to demonstrate how that's possible: + for (const auto& b : rootTextBlock.Blocks()) + { + if (const auto& p{ b.try_as() }) + { + for (const auto& line : p.Inlines()) + { + if (const auto& otherContent{ line.try_as() }) + { + if (const auto& codeBlock{ otherContent.Child().try_as() }) + { + codeBlock.PlayButtonVisibility(WUX::Visibility::Visible); + codeBlock.RequestRunCommands({ this, &MarkdownPaneContent::_handleRunCommandRequest }); + } + } + } + } + } + + RenderedMarkdown().Children().Append(rootTextBlock); + } + + void MarkdownPaneContent::_loadTapped(const Windows::Foundation::IInspectable&, const Windows::UI::Xaml::Input::TappedRoutedEventArgs&) + { + _filePath = FilePathInput().Text(); + // Does the file exist? if not, bail + const wil::unique_handle file{ CreateFileW(_filePath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, nullptr) }; + if (!file) + { + return; + } + + // It does. Clear the old one + _clearOldNotebook(); + _loadFile(); + } + + void MarkdownPaneContent::_editTapped(const Windows::Foundation::IInspectable&, const Windows::UI::Xaml::Input::TappedRoutedEventArgs&) + { + if (Editing()) + { + _clearOldNotebook(); + _renderFileContents(); + + EditIcon().Glyph(L"\xe932"); // Label + + _scrollViewer().Visibility(WUX::Visibility::Visible); + _editor().Visibility(WUX::Visibility::Collapsed); + + Editing(false); + } + else + { + EditIcon().Glyph(L"\xe890"); // View + + _scrollViewer().Visibility(WUX::Visibility::Collapsed); + _editor().Visibility(WUX::Visibility::Visible); + + Editing(true); + } + PropertyChanged.raise(*this, WUX::Data::PropertyChangedEventArgs{ L"Editing" }); + } + + void MarkdownPaneContent::_closeTapped(const Windows::Foundation::IInspectable&, const Windows::UI::Xaml::Input::TappedRoutedEventArgs&) + { + CloseRequested.raise(*this, nullptr); + } + + void MarkdownPaneContent::_handleRunCommandRequest(const Microsoft::Terminal::UI::Markdown::CodeBlock& /*sender*/, + const Microsoft::Terminal::UI::Markdown::RequestRunCommandsArgs& request) + { + auto text = request.Commandlines(); + + if (const auto& strongControl{ _control.get() }) + { + Model::ActionAndArgs actionAndArgs{ ShortcutAction::SendInput, Model::SendInputArgs{ text } }; + + // By using the last active control as the sender here, the + // action dispatch will send this to the active control, + // thinking that it is the control that requested this event. + DispatchActionRequested.raise(strongControl, actionAndArgs); + strongControl.Focus(winrt::WUX::FocusState::Programmatic); + } + } + +#pragma region IPaneContent + + winrt::Windows::UI::Xaml::FrameworkElement MarkdownPaneContent::GetRoot() + { + return *this; + } + + void MarkdownPaneContent::Close() + { + CloseRequested.raise(*this, nullptr); + } + + winrt::hstring MarkdownPaneContent::Icon() const + { + static constexpr std::wstring_view glyph{ L"\xe70b" }; // QuickNote + return winrt::hstring{ glyph }; + } + +#pragma endregion + + void MarkdownPaneContent::SetLastActiveControl(const Microsoft::Terminal::Control::TermControl& control) + { + _control = control; + } +} diff --git a/src/cascadia/TerminalApp/MarkdownPaneContent.h b/src/cascadia/TerminalApp/MarkdownPaneContent.h new file mode 100644 index 00000000000..e8fa4cb0ce8 --- /dev/null +++ b/src/cascadia/TerminalApp/MarkdownPaneContent.h @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "MarkdownPaneContent.g.h" +#include "BasicPaneEvents.h" + +namespace winrt::TerminalApp::implementation +{ + struct MarkdownPaneContent : MarkdownPaneContentT, BasicPaneEvents + { + public: + MarkdownPaneContent(); + MarkdownPaneContent(const winrt::hstring& filePath); + + til::property Editing{ false }; + til::property FileContents{ L"" }; + + void SetLastActiveControl(const Microsoft::Terminal::Control::TermControl& control); + + // TODO! this should just be til::property_changed_event but I don't have that commit here + til::event PropertyChanged; + +#pragma region IPaneContent + winrt::Windows::UI::Xaml::FrameworkElement GetRoot(); + + void UpdateSettings(const winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings&){}; + + winrt::Windows::Foundation::Size MinimumSize() { return { 1, 1 }; }; + void Focus(winrt::Windows::UI::Xaml::FocusState reason = winrt::Windows::UI::Xaml::FocusState::Programmatic) { reason; }; + void Close(); + winrt::Microsoft::Terminal::Settings::Model::INewContentArgs GetNewTerminalArgs(BuildStartupKind kind) const; + + winrt::hstring Title() { return _filePath; } + uint64_t TaskbarState() { return 0; } + uint64_t TaskbarProgress() { return 0; } + bool ReadOnly() { return false; } + winrt::hstring Icon() const; + Windows::Foundation::IReference TabColor() const noexcept { return nullptr; } + winrt::Windows::UI::Xaml::Media::Brush BackgroundBrush() { return Background(); } + + // See BasicPaneEvents for most generic event definitions + +#pragma endregion + + til::typed_event DispatchActionRequested; + + void _handleRunCommandRequest(const Microsoft::Terminal::UI::Markdown::CodeBlock& sender, + const Microsoft::Terminal::UI::Markdown::RequestRunCommandsArgs& control); + + private: + friend struct MarkdownPaneContentT; // for Xaml to bind events + + winrt::hstring _filePath{}; + + winrt::weak_ref _control{ nullptr }; + + void _clearOldNotebook(); + void _loadFile(); + void _renderFileContents(); + void _loadText(); + void _loadMarkdown(); + + void _loadTapped(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::TappedRoutedEventArgs& e); + void _editTapped(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::TappedRoutedEventArgs& e); + void _closeTapped(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::TappedRoutedEventArgs& e); + }; +} + +namespace winrt::TerminalApp::factory_implementation +{ + BASIC_FACTORY(MarkdownPaneContent); +} diff --git a/src/cascadia/TerminalApp/MarkdownPaneContent.idl b/src/cascadia/TerminalApp/MarkdownPaneContent.idl new file mode 100644 index 00000000000..224c1c6b912 --- /dev/null +++ b/src/cascadia/TerminalApp/MarkdownPaneContent.idl @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "IPaneContent.idl"; +import "FilteredCommand.idl"; + +namespace TerminalApp +{ + [default_interface] runtimeclass MarkdownPaneContent : Windows.UI.Xaml.Controls.UserControl, + IPaneContent, + Windows.UI.Xaml.Data.INotifyPropertyChanged + { + MarkdownPaneContent(); + MarkdownPaneContent(String originalPath); + void SetLastActiveControl(Microsoft.Terminal.Control.TermControl control); + + Boolean Editing; + String FileContents; + + event Windows.Foundation.TypedEventHandler DispatchActionRequested; + + } + +} diff --git a/src/cascadia/TerminalApp/MarkdownPaneContent.xaml b/src/cascadia/TerminalApp/MarkdownPaneContent.xaml new file mode 100644 index 00000000000..54f732ce518 --- /dev/null +++ b/src/cascadia/TerminalApp/MarkdownPaneContent.xaml @@ -0,0 +1,129 @@ + + + + + + + + + #282828 + #90ef90 + #8888 + + + #F9F9F9 + #257f01 + #88222222 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/TerminalApp/SnippetsPaneContent.xaml b/src/cascadia/TerminalApp/SnippetsPaneContent.xaml index f7184c96cf7..0d7b339531c 100644 --- a/src/cascadia/TerminalApp/SnippetsPaneContent.xaml +++ b/src/cascadia/TerminalApp/SnippetsPaneContent.xaml @@ -206,6 +206,10 @@ + + diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index 99ccf110ff0..a1c6a4e6a8f 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj @@ -75,6 +75,9 @@ Designer + + Designer + @@ -178,6 +181,9 @@ SuggestionsControl.xaml + + MarkdownPaneContent.xaml + @@ -296,6 +302,11 @@ SuggestionsControl.xaml + + MarkdownPaneContent.xaml + Code + + @@ -368,6 +379,10 @@ Code + + MarkdownPaneContent.xaml + Code + @@ -416,6 +431,11 @@ {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F} + + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F} + true + false + @@ -459,6 +479,12 @@ false false + + $(OpenConsoleCommonOutDir)Microsoft.Terminal.UI.Markdown\Microsoft.Terminal.UI.Markdown.winmd + true + false + false + true false @@ -504,4 +530,4 @@ - \ No newline at end of file + diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 99064fef8bb..56c5af26c38 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -17,6 +17,7 @@ #include "SettingsPaneContent.h" #include "ScratchpadContent.h" #include "SnippetsPaneContent.h" +#include "MarkdownPaneContent.h" #include "TabRowControl.h" #include "TerminalPage.g.cpp" @@ -3438,6 +3439,30 @@ namespace winrt::TerminalApp::implementation content = *tasksContent; } + else if (paneType == L"x-markdown") + { + if (Feature_MarkdownPane::IsEnabled()) + { + const auto& markdownContent{ winrt::make_self(L"") }; + markdownContent->UpdateSettings(_settings); + markdownContent->GetRoot().KeyDown({ this, &TerminalPage::_KeyDownHandler }); + + // This one doesn't use DispatchCommand, because we don't create + // Command's freely at runtime like we do with just plain old actions. + markdownContent->DispatchActionRequested([weak = get_weak()](const auto& sender, const auto& actionAndArgs) { + if (const auto& page{ weak.get() }) + { + page->_actionDispatch->DoAction(sender, actionAndArgs); + } + }); + if (const auto& termControl{ _GetActiveControl() }) + { + markdownContent->SetLastActiveControl(termControl); + } + + content = *markdownContent; + } + } assert(content); diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index e81e452708e..ca50edefe09 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -1235,6 +1235,10 @@ namespace winrt::TerminalApp::implementation { taskPane.SetLastActiveControl(termControl); } + else if (const auto& taskPane{ p->GetContent().try_as() }) + { + taskPane.SetLastActiveControl(termControl); + } }); } } diff --git a/src/cascadia/TerminalApp/dll/TerminalApp.vcxproj b/src/cascadia/TerminalApp/dll/TerminalApp.vcxproj index fef17937176..8e158d51da3 100644 --- a/src/cascadia/TerminalApp/dll/TerminalApp.vcxproj +++ b/src/cascadia/TerminalApp/dll/TerminalApp.vcxproj @@ -69,6 +69,7 @@ + {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F} diff --git a/src/cascadia/TerminalApp/pch.h b/src/cascadia/TerminalApp/pch.h index ec2655d349f..394990ee34d 100644 --- a/src/cascadia/TerminalApp/pch.h +++ b/src/cascadia/TerminalApp/pch.h @@ -63,6 +63,8 @@ #include #include #include +#include + #include #include #include diff --git a/src/cascadia/UIMarkdown/Builder.cpp b/src/cascadia/UIMarkdown/Builder.cpp new file mode 100644 index 00000000000..8639dcc6f25 --- /dev/null +++ b/src/cascadia/UIMarkdown/Builder.cpp @@ -0,0 +1,15 @@ +#include "pch.h" +#include "Builder.h" +#include "Builder.g.cpp" + +#include "MarkdownToXaml.h" + +namespace winrt::Microsoft::Terminal::UI::Markdown::implementation +{ + winrt::Windows::UI::Xaml::Controls::RichTextBlock Builder::Convert(const winrt::hstring& text, + const winrt::hstring& baseUrl) + { + const auto u8String{ til::u16u8(text) }; + return MarkdownToXaml::Convert(u8String, baseUrl); + } +} diff --git a/src/cascadia/UIMarkdown/Builder.h b/src/cascadia/UIMarkdown/Builder.h new file mode 100644 index 00000000000..9d22bf4bb94 --- /dev/null +++ b/src/cascadia/UIMarkdown/Builder.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "Builder.g.h" + +namespace winrt::Microsoft::Terminal::UI::Markdown::implementation +{ + struct Builder + { + static winrt::Windows::UI::Xaml::Controls::RichTextBlock Convert(const winrt::hstring& text, const winrt::hstring& baseUrl); + }; +} + +namespace winrt::Microsoft::Terminal::UI::Markdown::factory_implementation +{ + BASIC_FACTORY(Builder); +} diff --git a/src/cascadia/UIMarkdown/Builder.idl b/src/cascadia/UIMarkdown/Builder.idl new file mode 100644 index 00000000000..5db6a75c34f --- /dev/null +++ b/src/cascadia/UIMarkdown/Builder.idl @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.Terminal.UI.Markdown +{ + static runtimeclass Builder + { + static Windows.UI.Xaml.Controls.RichTextBlock Convert(String text, String baseUrl); + }; + +} diff --git a/src/cascadia/UIMarkdown/CodeBlock.cpp b/src/cascadia/UIMarkdown/CodeBlock.cpp new file mode 100644 index 00000000000..0644157d8e5 --- /dev/null +++ b/src/cascadia/UIMarkdown/CodeBlock.cpp @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "CodeBlock.h" +#include + +#include "CodeBlock.g.cpp" +#include "RequestRunCommandsArgs.g.cpp" + +namespace winrt +{ + namespace MUX = Microsoft::UI::Xaml; + namespace WUX = Windows::UI::Xaml; + using IInspectable = Windows::Foundation::IInspectable; +} + +namespace winrt::Microsoft::Terminal::UI::Markdown::implementation +{ + CodeBlock::CodeBlock(const winrt::hstring& initialCommandlines) : + Commandlines(initialCommandlines) + { + } + void CodeBlock::_playPressed(const Windows::Foundation::IInspectable&, + const Windows::UI::Xaml::Input::TappedRoutedEventArgs& e) + { + auto args = winrt::make_self(Commandlines()); + RequestRunCommands.raise(*this, *args); + e.Handled(true); + } +} diff --git a/src/cascadia/UIMarkdown/CodeBlock.h b/src/cascadia/UIMarkdown/CodeBlock.h new file mode 100644 index 00000000000..e80f45bf47a --- /dev/null +++ b/src/cascadia/UIMarkdown/CodeBlock.h @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "CodeBlock.g.h" +#include "RequestRunCommandsArgs.g.h" +#include "../../../src/cascadia/inc/cppwinrt_utils.h" +#include +#include + +namespace winrt::Microsoft::Terminal::UI::Markdown::implementation +{ + struct CodeBlock : CodeBlockT + { + CodeBlock(const winrt::hstring& initialCommandlines); + + til::property Commandlines; + + til::property_changed_event PropertyChanged; + til::typed_event RequestRunCommands; + + WINRT_OBSERVABLE_PROPERTY(Windows::UI::Xaml::Visibility, PlayButtonVisibility, PropertyChanged.raise, Windows::UI::Xaml::Visibility::Collapsed); + + private: + friend struct CodeBlockT; // for Xaml to bind events + + void _playPressed(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::TappedRoutedEventArgs& e); + }; + + struct RequestRunCommandsArgs : RequestRunCommandsArgsT + { + RequestRunCommandsArgs(const winrt::hstring& commandlines) : + Commandlines{ commandlines } {}; + + til::property Commandlines; + }; +} + +namespace winrt::Microsoft::Terminal::UI::Markdown::factory_implementation +{ + BASIC_FACTORY(CodeBlock); +} diff --git a/src/cascadia/UIMarkdown/CodeBlock.idl b/src/cascadia/UIMarkdown/CodeBlock.idl new file mode 100644 index 00000000000..61ffdd27dce --- /dev/null +++ b/src/cascadia/UIMarkdown/CodeBlock.idl @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.Terminal.UI.Markdown +{ + runtimeclass RequestRunCommandsArgs + { + String Commandlines { get;}; + } + + [default_interface] runtimeclass CodeBlock : Windows.UI.Xaml.Controls.UserControl, + Windows.UI.Xaml.Data.INotifyPropertyChanged + { + CodeBlock(String initialCommandlines); + + String Commandlines { get; set; }; + Windows.UI.Xaml.Visibility PlayButtonVisibility { get; set; }; + + event Windows.Foundation.TypedEventHandler RequestRunCommands; + }; + +} diff --git a/src/cascadia/UIMarkdown/CodeBlock.xaml b/src/cascadia/UIMarkdown/CodeBlock.xaml new file mode 100644 index 00000000000..009c965458f --- /dev/null +++ b/src/cascadia/UIMarkdown/CodeBlock.xaml @@ -0,0 +1,210 @@ + + + + + + + + #1e1e1e + #30363d + #90ef90 + #88888888 + + + #f6f8fa + #d3d3d3 + #257f01 + #88222222 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/UIMarkdown/MarkdownToXaml.cpp b/src/cascadia/UIMarkdown/MarkdownToXaml.cpp new file mode 100644 index 00000000000..8933eb6a56f --- /dev/null +++ b/src/cascadia/UIMarkdown/MarkdownToXaml.cpp @@ -0,0 +1,469 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "CodeBlock.h" +#include "MarkdownToXaml.h" + +#include + +namespace winrt +{ + namespace MUX = Microsoft::UI::Xaml; + namespace WUX = Windows::UI::Xaml; + using IInspectable = Windows::Foundation::IInspectable; +} +using namespace winrt; + +// Bullet points used for unordered lists. +static constexpr std::wstring_view bullets[]{ + L"• ", + L"◦ ", + L"▪ " // After this level, we'll keep using this one. +}; +static constexpr int WidthOfBulletPoint{ 9 }; +static constexpr int IndentWidth{ 3 * WidthOfBulletPoint }; +static constexpr int H1FontSize{ 36 }; +static constexpr int HeaderMinFontSize{ 16 }; + +static constexpr std::wstring_view CodeFontFamily{ L"Cascadia Mono, Consolas" }; + +template +static std::string_view textFromCmarkString(const T& s) noexcept +{ + return std::string_view{ (char*)s.data, (size_t)s.len }; +} +static std::string_view textFromLiteral(cmark_node* node) noexcept +{ + return cmark_node_get_literal(node); +} +static std::string_view textFromUrl(cmark_node* node) noexcept +{ + return cmark_node_get_url(node); +} + +typedef wil::unique_any unique_node; +typedef wil::unique_any unique_iter; + +// Function Description: +// - Entrypoint to convert a string of markdown into a XAML RichTextBlock. +// Arguments: +// - markdownText: the markdown content to render +// - baseUrl: the current URI of the content. This will allow for relative links +// to be appropriately resolved. +// Return Value: +// - a RichTextBlock with the rendered markdown in it. +WUX::Controls::RichTextBlock MarkdownToXaml::Convert(std::string_view markdownText, const winrt::hstring& baseUrl) +{ + MarkdownToXaml data{ baseUrl }; + + unique_node doc{ cmark_parse_document(markdownText.data(), markdownText.size(), CMARK_OPT_DEFAULT) }; + unique_iter iter{ cmark_iter_new(doc.get()) }; + cmark_event_type ev_type; + + while ((ev_type = cmark_iter_next(iter.get())) != CMARK_EVENT_DONE) + { + data._RenderNode(cmark_iter_get_node(iter.get()), ev_type); + } + + return data._root; +} + +MarkdownToXaml::MarkdownToXaml(const winrt::hstring& baseUrl) : + _baseUri{ baseUrl } +{ + _root.IsTextSelectionEnabled(true); + _root.TextWrapping(WUX::TextWrapping::WrapWholeWords); +} + +WUX::Documents::Paragraph MarkdownToXaml::_CurrentParagraph() +{ + if (_lastParagraph == nullptr) + { + _lastParagraph = WUX::Documents::Paragraph{}; + if (_indent > 0) + { + // If we're in a list, we will start this paragraph with a bullet + // point. That bullet point will be added as part of the actual text + // of the paragraph, but we want the real text of the paragraph all + // aligned. So we will _de-indent_ the first line, to give us space + // for the bullet. + if (_indent - _blockQuoteDepth > 0) + { + _lastParagraph.TextIndent(-WidthOfBulletPoint); + } + _lastParagraph.Margin(WUX::ThicknessHelper::FromLengths(IndentWidth * _indent, 0, 0, 0)); + } + _root.Blocks().Append(_lastParagraph); + } + return _lastParagraph; +} +WUX::Documents::Run MarkdownToXaml::_CurrentRun() +{ + if (_lastRun == nullptr) + { + _lastRun = WUX::Documents::Run{}; + _CurrentSpan().Inlines().Append(_lastRun); + } + return _lastRun; +} +WUX::Documents::Span MarkdownToXaml::_CurrentSpan() +{ + if (_lastSpan == nullptr) + { + _lastSpan = WUX::Documents::Span{}; + _CurrentParagraph().Inlines().Append(_lastSpan); + } + return _lastSpan; +} +WUX::Documents::Run MarkdownToXaml::_NewRun() +{ + if (_lastRun == nullptr) + { + return _CurrentRun(); + } + else + { + auto old{ _lastRun }; + + WUX::Documents::Run newRun{}; + + newRun.FontFamily(old.FontFamily()); + newRun.FontWeight(old.FontWeight()); + newRun.FontStyle(old.FontStyle()); + + _lastRun = newRun; + _CurrentSpan().Inlines().Append(_lastRun); + } + return _lastRun; +} +void MarkdownToXaml::_EndRun() +{ + _lastRun = nullptr; +} +void MarkdownToXaml::_EndSpan() +{ + _EndRun(); + _lastSpan = nullptr; +} +void MarkdownToXaml::_EndParagraph() +{ + _EndSpan(); + _lastParagraph = nullptr; +} + +WUX::Controls::TextBlock MarkdownToXaml::_makeDefaultTextBlock() +{ + WUX::Controls::TextBlock b{}; + b.IsTextSelectionEnabled(true); + b.TextWrapping(WUX::TextWrapping::WrapWholeWords); + return b; +} + +void MarkdownToXaml::_RenderNode(cmark_node* node, cmark_event_type ev_type) +{ + bool entering = (ev_type == CMARK_EVENT_ENTER); + + switch (cmark_node_get_type(node)) + { + case CMARK_NODE_DOCUMENT: + break; + + case CMARK_NODE_BLOCK_QUOTE: + + // It's non-trivial to deal with the right-side vertical lines that + // we're accustomed to seeing for block quotes in markdown content. + // RichTextBlock doesn't have a good way of adding a border to a + // paragraph, it would seem. + // + // We could add a InlineUIContainer, with a Border in there, then + // put a new RichTextBlock in there, but I believe text selection + // wouldn't transit across the border. + + // Instead, we're just going to add a new layer of indenting. + + if (entering) + { + _EndParagraph(); + _indent++; + _blockQuoteDepth++; + } + else + { + _EndParagraph(); + _indent = std::max(0, _indent - 1); + _blockQuoteDepth = std::max(0, _blockQuoteDepth - 1); + } + + break; + + case CMARK_NODE_LIST: + { + // when `node->as.list.list_type == CMARK_BULLET_LIST`, we're an unordered list. + // Otherwise, we're an ordered one (and we might not start at 0). + // + // However, we don't support numbered lists for now. + if (entering) + { + _EndParagraph(); + _indent++; + } + else + { + _EndParagraph(); + _indent = std::max(0, _indent - 1); + } + break; + } + + case CMARK_NODE_ITEM: + // A list item, either for a ordered list or an unordered one. + if (entering) + { + _EndParagraph(); + _NewRun().Text(bullets[std::clamp(_indent - _blockQuoteDepth - 1, 0, 2)]); + } + break; + + case CMARK_NODE_HEADING: + { + _EndParagraph(); + + // At the start of a header, change the font size to match the new + // level of header we're at. The text will come later, in a + // CMARK_NODE_TEXT + if (entering) + { + // Insert a blank line, just to help break up the walls of text. + // This better reflects the way MD is rendered to HTML + _root.Blocks().Append(WUX::Documents::Paragraph{}); + + const auto level = cmark_node_get_heading_level(node); + _CurrentParagraph().FontSize(std::max(HeaderMinFontSize, H1FontSize - level * 6)); + } + break; + } + + case CMARK_NODE_CODE_BLOCK: + { + _EndParagraph(); + + const auto codeHstring{ winrt::to_hstring(cmark_node_get_literal(node)) }; + // The literal for a code node always includes the trailing newline. + // Trim that off. + std::wstring_view codeView{ codeHstring.c_str(), codeHstring.size() - 1 }; + + auto codeBlock = winrt::make(winrt::hstring{ codeView }); + WUX::Documents::InlineUIContainer codeContainer{}; + codeContainer.Child(codeBlock); + _CurrentParagraph().Inlines().Append(codeContainer); + + _EndParagraph(); + } + break; + + case CMARK_NODE_HTML_BLOCK: + // Raw HTML comes to us in the literal + // node->as.literal.data, node->as.literal.len + // But we don't support raw HTML, so we'll do nothing. + break; + + case CMARK_NODE_CUSTOM_BLOCK: + // Not even entirely sure what this is. + break; + + case CMARK_NODE_THEMATIC_BREAK: + // A
. Not currently supported. + break; + + case CMARK_NODE_PARAGRAPH: + { + bool tight; + cmark_node* parent = cmark_node_parent(node); + cmark_node* grandparent = cmark_node_parent(parent); + + if (grandparent != NULL && cmark_node_get_type(grandparent)) + { + tight = cmark_node_get_list_tight(grandparent); + } + else + { + tight = false; + } + + // If we aren't in a list, then end the current paragraph and + // start a new one. + if (!tight) + { + _EndParagraph(); + } + + // Start a new paragraph if we don't have one + break; + } + case CMARK_NODE_TEXT: + { + const auto text{ winrt::to_hstring(textFromLiteral(node)) }; + + if (_lastImage) + { + // The tooltip for an image comes in as a CMARK_NODE_TEXT, so set that here. + WUX::Controls::ToolTipService::SetToolTip(_lastImage, box_value(text)); + } + else + { + // Otherwise, just add the text to the current paragraph + _NewRun().Text(text); + } + } + + break; + + case CMARK_NODE_LINEBREAK: + _EndSpan(); + _CurrentParagraph().Inlines().Append(WUX::Documents::LineBreak()); + break; + + case CMARK_NODE_SOFTBREAK: + // I'm fairly confident this is what happens when you've just got + // two lines only separated by a single \r\n in a MD doc. E.g. when + // you want a paragraph to wrap at 80 columns in code, but wrap in + // the rendered document. + // + // In the HTML implementation, what happens here depends on the options: + // * CMARK_OPT_HARDBREAKS: add a full line break + // * CMARK_OPT_NOBREAKS: Just add a space + // * otherwise, just add a '\n' + // + // We're not really messing with options here, so lets just add a + // space. That seems to keep the current line going, but allow for + // word breaking. + + _NewRun().Text(L" "); + break; + + case CMARK_NODE_CODE: + { + const auto text{ winrt::to_hstring(textFromLiteral(node)) }; + const auto& codeRun{ _NewRun() }; + + codeRun.FontFamily(WUX::Media::FontFamily{ CodeFontFamily }); + // A Span can't have a border or a background, so we can't give + // it the whole treatment that a span gets in HTML. + codeRun.Text(text); + + _NewRun().FontFamily(_root.FontFamily()); + } + break; + + case CMARK_NODE_HTML_INLINE: + // Same as above - no raw HTML support here. + break; + + case CMARK_NODE_CUSTOM_INLINE: + // Same as above - not even entirely sure what this is. + break; + + case CMARK_NODE_STRONG: + _NewRun().FontWeight(entering ? + winrt::Windows::UI::Text::FontWeights::Bold() : + winrt::Windows::UI::Text::FontWeights::Normal()); + break; + + case CMARK_NODE_EMPH: + _NewRun().FontStyle(entering ? + winrt::Windows::UI::Text::FontStyle::Italic : + winrt::Windows::UI::Text::FontStyle::Normal); + break; + + case CMARK_NODE_LINK: + + if (entering) + { + const auto urlHstring{ to_hstring(textFromUrl(node)) }; + WUX::Documents::Hyperlink a{}; + + // Set the tooltip to display the URL + try + { + // This block is from TermControl.cpp, where we sanitize the + // tooltips for URLs. That has a much more comprehensive + // comment. + + const winrt::Windows::Foundation::Uri uri{ _baseUri, urlHstring }; + + a.NavigateUri(uri); + + auto tooltipText = urlHstring; + const auto unicode = uri.AbsoluteUri(); + const auto punycode = uri.AbsoluteCanonicalUri(); + if (punycode != unicode) + { + tooltipText = winrt::hstring{ punycode + L"\n" + unicode }; + } + WUX::Controls::ToolTipService::SetToolTip(a, box_value(tooltipText)); + } + catch (...) + { + } + + _CurrentParagraph().Inlines().Append(a); + _lastSpan = a; + + // Similar to the header element, the actual text of the link + // will later come through as a CMARK_NODE_TEXT + } + else + { + _EndSpan(); + } + break; + + case CMARK_NODE_IMAGE: + if (entering) + { + const auto urlHstring{ to_hstring(textFromUrl(node)) }; + + try + { + winrt::Windows::Foundation::Uri uri{ _baseUri, urlHstring }; + + WUX::Media::Imaging::BitmapImage bitmapImage; + bitmapImage.UriSource(uri); + + WUX::Controls::Image img{}; + img.Source(bitmapImage); + + WUX::Documents::InlineUIContainer imageBlock{}; + imageBlock.Child(img); + + _CurrentParagraph().Inlines().Append(imageBlock); + _lastImage = img; + } + catch (...) + { + } + } + else + { + _EndSpan(); + _lastImage = nullptr; + } + break; + + // These elements are in cmark-gfm, which we'd love to move to in the + // future, but isn't yet available in vcpkg. + + // case CMARK_NODE_FOOTNOTE_DEFINITION: + // // Not supported currently + // break; + // + // case CMARK_NODE_FOOTNOTE_REFERENCE: + // // Not supported currently + // break; + + default: + assert(false); + break; + } +} diff --git a/src/cascadia/UIMarkdown/MarkdownToXaml.h b/src/cascadia/UIMarkdown/MarkdownToXaml.h new file mode 100644 index 00000000000..d5e3c71dc38 --- /dev/null +++ b/src/cascadia/UIMarkdown/MarkdownToXaml.h @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include + +struct MarkdownToXaml +{ +public: + static winrt::Windows::UI::Xaml::Controls::RichTextBlock Convert(std::string_view markdownText, const winrt::hstring& baseUrl); + +private: + MarkdownToXaml(const winrt::hstring& baseUrl); + + winrt::hstring _baseUri{ L"" }; + + winrt::Windows::UI::Xaml::Controls::RichTextBlock _root{}; + winrt::Windows::UI::Xaml::Documents::Run _lastRun{ nullptr }; + winrt::Windows::UI::Xaml::Documents::Span _lastSpan{ nullptr }; + winrt::Windows::UI::Xaml::Documents::Paragraph _lastParagraph{ nullptr }; + winrt::Windows::UI::Xaml::Controls::Image _lastImage{ nullptr }; + + int _indent = 0; + int _blockQuoteDepth = 0; + + winrt::Windows::UI::Xaml::Documents::Paragraph _CurrentParagraph(); + winrt::Windows::UI::Xaml::Documents::Run _CurrentRun(); + winrt::Windows::UI::Xaml::Documents::Span _CurrentSpan(); + winrt::Windows::UI::Xaml::Documents::Run _NewRun(); + void _EndRun(); + void _EndSpan(); + void _EndParagraph(); + + winrt::Windows::UI::Xaml::Controls::TextBlock _makeDefaultTextBlock(); + + void _RenderNode(cmark_node* node, cmark_event_type ev_type); +}; diff --git a/src/cascadia/UIMarkdown/Microsoft.Terminal.UI.Markdown.def b/src/cascadia/UIMarkdown/Microsoft.Terminal.UI.Markdown.def new file mode 100644 index 00000000000..ba15818ddb1 --- /dev/null +++ b/src/cascadia/UIMarkdown/Microsoft.Terminal.UI.Markdown.def @@ -0,0 +1,3 @@ +EXPORTS +DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE +DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE diff --git a/src/cascadia/UIMarkdown/UIMarkdown.vcxproj b/src/cascadia/UIMarkdown/UIMarkdown.vcxproj new file mode 100644 index 00000000000..66d61d3ad77 --- /dev/null +++ b/src/cascadia/UIMarkdown/UIMarkdown.vcxproj @@ -0,0 +1,87 @@ + + + + {7615F03F-E56D-4DB4-B23D-BD4FB80DB36F} + Microsoft.Terminal.UI.Markdown + Microsoft.Terminal.UI.Markdown + DynamicLibrary + Console + true + + + 4 + nested + + + + true + true + + + + + + + + Builder.idl + + + + CodeBlock.xaml + + + + + + Create + + + Builder.idl + + + CodeBlock.xaml + Code + + + + + + + + CodeBlock.xaml + Code + + + + + Designer + + + + + + + {18D09A24-8240-42D6-8CB6-236EEE820263} + + + {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE} + false + + + + + + + + diff --git a/src/cascadia/UIMarkdown/UIMarkdown.vcxproj.filters b/src/cascadia/UIMarkdown/UIMarkdown.vcxproj.filters new file mode 100644 index 00000000000..ccbde7f1f8f --- /dev/null +++ b/src/cascadia/UIMarkdown/UIMarkdown.vcxproj.filters @@ -0,0 +1,38 @@ + + + + + accd3aa8-1ba0-4223-9bbe-0c431709210b + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tga;tiff;tif;png;wav;mfcribbon-ms + + + {926ab91d-31b4-48c3-b9a4-e681349f27f0} + + + {ee3ff32f-d013-49cb-8eb9-1ec084585086} + + + {091e7617-c506-4742-82fc-75e7ca32e2fe} + + + + + Module + + + Module + + + Module + + + + + + + + + + + + diff --git a/src/cascadia/UIMarkdown/init.cpp b/src/cascadia/UIMarkdown/init.cpp new file mode 100644 index 00000000000..689f4b67f61 --- /dev/null +++ b/src/cascadia/UIMarkdown/init.cpp @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. + +#include "pch.h" + +#pragma warning(suppress : 26440) // Not interested in changing the specification of DllMain to make it noexcept given it's an interface to the OS. +BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD reason, LPVOID /*reserved*/) +{ + switch (reason) + { + case DLL_PROCESS_ATTACH: + DisableThreadLibraryCalls(hInstDll); + break; + case DLL_PROCESS_DETACH: + break; + default: + break; + } + + return TRUE; +} diff --git a/src/cascadia/UIMarkdown/pch.cpp b/src/cascadia/UIMarkdown/pch.cpp new file mode 100644 index 00000000000..398a99f6653 --- /dev/null +++ b/src/cascadia/UIMarkdown/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" diff --git a/src/cascadia/UIMarkdown/pch.h b/src/cascadia/UIMarkdown/pch.h new file mode 100644 index 00000000000..aa6b7fa4850 --- /dev/null +++ b/src/cascadia/UIMarkdown/pch.h @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// pch.h +// Header for platform projection include files +// + +#pragma once + +#define WIN32_LEAN_AND_MEAN +#define NOMCX +#define NOHELP +#define NOCOMM + +// Manually include til after we include Windows.Foundation to give it winrt superpowers +#define BLOCK_TIL +#include +// This is inexplicable, but for whatever reason, cppwinrt conflicts with the +// SDK definition of this function, so the only fix is to undef it. +// from WinBase.h +// Windows::UI::Xaml::Media::Animation::IStoryboard::GetCurrentTime +#ifdef GetCurrentTime +#undef GetCurrentTime +#endif + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +// Manually include til after we include Windows.Foundation to give it winrt superpowers +#include "til.h" + +#include +#include // must go after the CoreDispatcher type is defined diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj index 21fea0cf5f7..7f3ab1aed0b 100644 --- a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj @@ -91,6 +91,7 @@ + diff --git a/src/features.xml b/src/features.xml index e0ac4743008..569acb9d6eb 100644 --- a/src/features.xml +++ b/src/features.xml @@ -145,6 +145,17 @@ + + Feature_MarkdownPane + Allow the user to create markdown panes. Experimental, to validate markdown parsing. + 16495 + AlwaysDisabled + + Dev + Canary + + + Feature_KeypadModeEnabled Enables the DECKPAM, DECKPNM sequences to work as intended diff --git a/vcpkg.json b/vcpkg.json index bd610a805e4..3536fad2105 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -9,7 +9,8 @@ "description": "Components required for Windows Terminal; separated out to make the Windows conhost build work", "dependencies": [ "jsoncpp", - "cli11" + "cli11", + "cmark" ] } }, @@ -29,6 +30,10 @@ { "name": "cli11", "version": "2.4.2" + }, + { + "name": "cmark", + "version": "0.30.3" } ], "builtin-baseline": "fe1cde61e971d53c9687cf9a46308f8f55da19fa"