// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Drawing;
using System.IO.Abstractions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using Common;
using Markdig;
using Microsoft.PowerToys.PreviewHandler.Markdown.Properties;
using Microsoft.PowerToys.PreviewHandler.Markdown.Telemetry.Events;
using Microsoft.PowerToys.Telemetry;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.WinForms;
using Windows.System;
namespace Microsoft.PowerToys.PreviewHandler.Markdown
{
///
/// Win Form Implementation for Markdown Preview Handler.
///
public class MarkdownPreviewHandlerControl : FormHandlerControl
{
private static readonly IFileSystem FileSystem = new FileSystem();
private static readonly IPath Path = FileSystem.Path;
private static readonly IFile File = FileSystem.File;
///
/// Extension to modify markdown AST.
///
private readonly HTMLParsingExtension _extension;
///
/// Markdig Pipeline builder.
///
private readonly MarkdownPipelineBuilder _pipelineBuilder;
///
/// Markdown HTML header for light theme.
///
private readonly string htmlLightHeader = "
";
///
/// Markdown HTML header for dark theme.
///
private readonly string htmlDarkHeader = "
";
///
/// Markdown HTML footer.
///
private readonly string htmlFooter = "
";
///
/// RichTextBox control to display if external images are blocked.
///
private RichTextBox _infoBar;
///
/// Extended Browser Control to display markdown html.
///
private WebView2 _browser;
///
/// WebView2 Environment
///
private CoreWebView2Environment _webView2Environment;
///
/// Name of the virtual host
///
public const string VirtualHostName = "PowerToysLocalMarkdown";
///
/// True if external image is blocked, false otherwise.
///
private bool _infoBarDisplayed;
///
/// Gets the path of the current assembly.
///
///
/// Source: https://stackoverflow.com/a/283917/14774889
///
public static string AssemblyDirectory
{
get
{
string codeBase = Assembly.GetExecutingAssembly().Location;
UriBuilder uri = new UriBuilder(codeBase);
string path = Uri.UnescapeDataString(uri.Path);
return Path.GetDirectoryName(path);
}
}
///
/// Initializes a new instance of the class.
///
public MarkdownPreviewHandlerControl()
{
// if you have a string with double space, some people view it as a new line.
// while this is against spec, even GH supports this. Technically looks like GH just trims whitespace
// https://github.com/microsoft/PowerToys/issues/10354
var softlineBreak = new Markdig.Extensions.Hardlines.SoftlineBreakAsHardlineExtension();
_extension = new HTMLParsingExtension(ImagesBlockedCallBack);
_pipelineBuilder = new MarkdownPipelineBuilder().UseAdvancedExtensions().UseEmojiAndSmiley().UseYamlFrontMatter().UseMathematics();
_pipelineBuilder.Extensions.Add(_extension);
_pipelineBuilder.Extensions.Add(softlineBreak);
}
///
/// Start the preview on the Control.
///
///
Path to the file.
public override void DoPreview
(T dataSource)
{
_infoBarDisplayed = false;
try
{
if (!(dataSource is string filePath))
{
throw new ArgumentException($"{nameof(dataSource)} for {nameof(MarkdownPreviewHandler)} must be a string but was a '{typeof(T)}'");
}
string fileText = File.ReadAllText(filePath);
Regex imageTagRegex = new Regex(@"<[ ]*img.*>");
if (imageTagRegex.IsMatch(fileText))
{
_infoBarDisplayed = true;
}
var htmlHeader = Common.UI.ThemeManager.GetWindowsBaseColor().ToLowerInvariant() == "dark" ? htmlDarkHeader : htmlLightHeader;
_extension.FilePath = Path.GetDirectoryName(filePath);
MarkdownPipeline pipeline = _pipelineBuilder.Build();
string parsedMarkdown = Markdig.Markdown.ToHtml(fileText, pipeline);
string markdownHTML = $"{htmlHeader}{parsedMarkdown}{htmlFooter}";
_browser = new WebView2()
{
Dock = DockStyle.Fill,
};
_browser.NavigationStarting += async (object sender, CoreWebView2NavigationStartingEventArgs args) =>
{
if (args.Uri != null && args.IsUserInitiated)
{
args.Cancel = true;
await Launcher.LaunchUriAsync(new Uri(args.Uri));
}
};
InvokeOnControlThread(() =>
{
ConfiguredTaskAwaitable.ConfiguredTaskAwaiter
webView2EnvironmentAwaiter = CoreWebView2Environment
.CreateAsync(userDataFolder: System.Environment.GetEnvironmentVariable("USERPROFILE") +
"\\AppData\\LocalLow\\Microsoft\\PowerToys\\MarkdownPreview-Temp")
.ConfigureAwait(true).GetAwaiter();
webView2EnvironmentAwaiter.OnCompleted(() =>
{
InvokeOnControlThread(async () =>
{
try
{
_webView2Environment = webView2EnvironmentAwaiter.GetResult();
await _browser.EnsureCoreWebView2Async(_webView2Environment).ConfigureAwait(true);
await _browser.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window.addEventListener('contextmenu', window => {window.preventDefault();});");
_browser.CoreWebView2.SetVirtualHostNameToFolderMapping(VirtualHostName, AssemblyDirectory, CoreWebView2HostResourceAccessKind.Allow);
_browser.NavigateToString(markdownHTML);
Controls.Add(_browser);
if (_infoBarDisplayed)
{
_infoBar = GetTextBoxControl(Resources.BlockedImageInfoText);
Resize += FormResized;
Controls.Add(_infoBar);
}
}
catch (NullReferenceException)
{
}
});
});
});
PowerToysTelemetry.Log.WriteEvent(new MarkdownFilePreviewed());
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
PowerToysTelemetry.Log.WriteEvent(new MarkdownFilePreviewError { Message = ex.Message });
InvokeOnControlThread(() =>
{
Controls.Clear();
_infoBarDisplayed = true;
_infoBar = GetTextBoxControl(Resources.MarkdownNotPreviewedError);
Resize += FormResized;
Controls.Add(_infoBar);
});
}
finally
{
base.DoPreview(dataSource);
}
}
///
/// Gets a textbox control.
///
/// Message to be displayed in textbox.
/// An object of type .
private RichTextBox GetTextBoxControl(string message)
{
RichTextBox richTextBox = new RichTextBox
{
Text = message,
BackColor = Color.LightYellow,
Multiline = true,
Dock = DockStyle.Top,
ReadOnly = true,
};
richTextBox.ContentsResized += RTBContentsResized;
richTextBox.ScrollBars = RichTextBoxScrollBars.None;
richTextBox.BorderStyle = BorderStyle.None;
return richTextBox;
}
///
/// Callback when RichTextBox is resized.
///
/// Reference to resized control.
/// Provides data for the resize event.
private void RTBContentsResized(object sender, ContentsResizedEventArgs e)
{
RichTextBox richTextBox = (RichTextBox)sender;
richTextBox.Height = e.NewRectangle.Height + 5;
}
///
/// Callback when form is resized.
///
/// Reference to resized control.
/// Provides data for the event.
private void FormResized(object sender, EventArgs e)
{
if (_infoBarDisplayed)
{
_infoBar.Width = Width;
}
}
///
/// Callback when image is blocked by extension.
///
private void ImagesBlockedCallBack()
{
_infoBarDisplayed = true;
}
}
}