From ec467f692461c0cd058783d10d295f976a01e099 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Sun, 9 Nov 2025 15:03:33 +0100 Subject: [PATCH] Blog post integrated --- Directory.Packages.props | 6 +- .../Settings.UI/PowerToys.Settings.csproj | 1 + .../OOBE/Views/ReleaseNotePage.xaml | 29 ++- .../OOBE/Views/ReleaseNotePage.xaml.cs | 173 +++++++++++++++++- 4 files changed, 199 insertions(+), 10 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3fe8abe8fb..cd70b12fea 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,7 +22,7 @@ - + @@ -73,7 +73,7 @@ - + @@ -125,4 +125,4 @@ - + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj index 9475120065..db5c1f3834 100644 --- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj +++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj @@ -67,6 +67,7 @@ + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotePage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotePage.xaml index 4a3f7a4720..015d866f02 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotePage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotePage.xaml @@ -19,13 +19,20 @@ H2Margin="0, 16, 0, 4" H3FontSize="16" H3FontWeight="SemiBold" - H3Margin="0, 16, 0, 4" /> + H3Margin="0, 16, 0, 4" + ImageMaxHeight="200" + ImageMaxWidth="600" + ImageStretch="Uniform" /> - + + + + + - + + + + + - + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotePage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotePage.xaml.cs index c9836da129..e84ff10f86 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotePage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/ReleaseNotePage.xaml.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// 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. @@ -6,7 +6,10 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; using System.Runtime.InteropServices.WindowsRuntime; +using System.Text.Json; +using System.Text.RegularExpressions; using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -16,13 +19,19 @@ using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.UI.Xaml.Navigation; +using ReverseMarkdown; using Windows.Foundation; using Windows.Foundation.Collections; +using Windows.System; +using static System.Net.WebRequestMethods; +using static System.Resources.ResXFileRef; namespace Microsoft.PowerToys.Settings.UI.OOBE.Views { public sealed partial class ReleaseNotePage : Page { + private static readonly HttpClient HttpClient = new(); + public ReleaseNotePage() { InitializeComponent(); @@ -34,7 +43,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views { MarkdownBlock.Text = item.Markdown ?? string.Empty; - if (!string.IsNullOrWhiteSpace(item.HeaderImageUri) && + /* if (!string.IsNullOrWhiteSpace(item.HeaderImageUri) && Uri.TryCreate(item.HeaderImageUri, UriKind.Absolute, out var uri)) { HeaderImage.Source = new BitmapImage(uri); @@ -44,10 +53,168 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views { HeaderImage.Source = null; HeaderImage.Visibility = Visibility.Collapsed; - } + } */ + + GetBlogData(); } base.OnNavigatedTo(e); } + + private static string NormalizePreCodeToFences(string html) + { + // ```lang\ncode\n``` + var rx = new Regex( + @"]*>\s*]*)>([\s\S]*?)\s*", + RegexOptions.IgnoreCase); + + string Repl(Match m) + { + var lang = m.Groups[1].Success ? m.Groups[1].Value : string.Empty; + var code = System.Net.WebUtility.HtmlDecode(m.Groups[2].Value); + return $"```{lang}\n{code.TrimEnd()}\n```\n\n"; + } + + // Also handle
without inner + var rxPre = new Regex(@"]*>([\s\S]*?)", RegexOptions.IgnoreCase); + + html = rx.Replace(html, Repl); + html = rxPre.Replace(html, m => + { + var txt = System.Net.WebUtility.HtmlDecode( + Regex.Replace(m.Groups[1].Value, "<.*?>", string.Empty, RegexOptions.Singleline)); + return $"```\n{txt.TrimEnd()}\n```\n\n"; + }); + + return html; + } + + private async void GetBlogData() + { + try + { + var url = "https://devblogs.microsoft.com/commandline/powertoys-0-94-is-here-settings-search-shortcut-conflict-detection-and-more/".Trim(); + if (string.IsNullOrWhiteSpace(url)) + { + return; + } + + // 1) Figure out the site base + slug from the URL + // Example: https://devblogs.microsoft.com/commandline// + var uri = new Uri(url); + var basePath = uri.AbsolutePath.Trim('/'); // "commandline/powertoys-0-94-..." + var firstSlash = basePath.IndexOf('/'); + if (firstSlash < 0) + { + throw new InvalidOperationException("Unexpected DevBlogs URL."); + } + + var site = basePath[..firstSlash]; // "commandline" + var slug = basePath[(firstSlash + 1)..].Trim('/'); // "powertoys-0-94-..." + + // 2) Call WordPress REST API for the sub-site + var api = $"https://devblogs.microsoft.com/{site}/wp-json/wp/v2/posts?slug={Uri.EscapeDataString(slug)}&_fields=title,content,link,date,slug,id"; + var json = await HttpClient.GetStringAsync(api); + + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + if (root.GetArrayLength() == 0) + { + throw new InvalidOperationException("Post not found. Check the URL/slug."); + } + + var post = root[0]; + var html = post.GetProperty("content").GetProperty("rendered").GetString() ?? string.Empty; + + // 3) Make image/anchor URLs absolute where needed + html = RewriteRelativeUrls(html, $"https://devblogs.microsoft.com/{site}"); + html = EnforceImageMaxWidth(html); + + // 3.1) Normalize
into fenced blocks + html = NormalizePreCodeToFences(html); + + // 4) HTML → Markdown + var config = new Config + { + GithubFlavored = true, + RemoveComments = true, + SmartHrefHandling = true, + }; + var converter = new ReverseMarkdown.Converter(config); + + var markdown = converter.Convert(html); + BlogTextBlock.Text = markdown; + } + catch (Exception ex) + { + BlogTextBlock.Text = $"**Error:** {ex.Message}"; + } + } + + private static string RewriteRelativeUrls(string html, string siteBase) + { + // Convert src/href that start with "/" or are site-relative to absolute +#pragma warning disable CA1310 // Specify StringComparison for correctness +#pragma warning disable CA1866 // Use char overload + string ToAbs(string url) => + url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) || + url.StartsWith("data:", StringComparison.OrdinalIgnoreCase) + ? url + : (url.StartsWith("/") ? $"https://devblogs.microsoft.com{url}" : + url.StartsWith("./") || url.StartsWith("../") ? new Uri(new Uri(siteBase + "/"), url).ToString() + : new Uri(new Uri(siteBase + "/"), url).ToString()); +#pragma warning restore CA1866 // Use char overload +#pragma warning restore CA1310 // Specify StringComparison for correctness + +#pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. +#pragma warning disable SA1117 // Parameters should be on same line or separate lines + html = Regex.Replace(html, "(?(?:src|href))=(\"|')(?[^\"']+)(\"|')", + m => $"{m.Groups["attr"].Value}=\"{ToAbs(m.Groups["url"].Value)}\"", + RegexOptions.IgnoreCase); +#pragma warning restore SA1117 // Parameters should be on same line or separate lines +#pragma warning restore SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. + + return html; + } + + private static string EnforceImageMaxWidth(string html, int maxWidth = 600) + { + return Regex.Replace( + html, + @"]*?)>", + m => + { + var tag = m.Value; + + // Skip if a style already contains max-width + if (Regex.IsMatch(tag, @"max-width\s*:\s*\d+", RegexOptions.IgnoreCase)) + { + return tag; + } + + // Inject style or append to existing one + if (Regex.IsMatch(tag, @"style\s*=", RegexOptions.IgnoreCase)) + { + return Regex.Replace( + tag, + @"style\s*=\s*(['""])(.*?)\1", + m2 => $"style={m2.Groups[1].Value}{m2.Groups[2].Value}; max-width:{maxWidth}px;{m2.Groups[1].Value}", + RegexOptions.IgnoreCase); + } + else + { + return tag.Insert(tag.Length - 1, $" style=\"max-width:{maxWidth}px; height:auto;\""); + } + }, + RegexOptions.IgnoreCase | RegexOptions.Singleline); + } + + private async void MarkdownView_OnLinkClicked(object sender, CommunityToolkit.WinUI.Controls.LinkClickedEventArgs e) + { + // Open links externally + await Launcher.LaunchUriAsync(e.Uri); + e.Handled = true; + } } }