mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 19:27:56 +01:00
Blog post integrated
This commit is contained in:
@@ -22,7 +22,7 @@
|
|||||||
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
|
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
|
||||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
|
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
|
||||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.251002-build.2316" />
|
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.251101-build.2372" />
|
||||||
<PackageVersion Include="ControlzEx" Version="6.0.0" />
|
<PackageVersion Include="ControlzEx" Version="6.0.0" />
|
||||||
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
||||||
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />
|
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
|
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
|
||||||
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
|
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
|
||||||
<PackageVersion Include="OpenAI" Version="2.0.0" />
|
<PackageVersion Include="OpenAI" Version="2.0.0" />
|
||||||
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
|
<PackageVersion Include="ReverseMarkdown" Version="4.7.1" />
|
||||||
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
|
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
|
||||||
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
|
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
|
||||||
<PackageVersion Include="SharpCompress" Version="0.37.2" />
|
<PackageVersion Include="SharpCompress" Version="0.37.2" />
|
||||||
@@ -125,4 +125,4 @@
|
|||||||
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
|
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
|
||||||
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
|
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -67,6 +67,7 @@
|
|||||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" />
|
<PackageReference Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" />
|
||||||
|
<PackageReference Include="ReverseMarkdown" />
|
||||||
<PackageReference Include="System.Net.Http" />
|
<PackageReference Include="System.Net.Http" />
|
||||||
<PackageReference Include="System.Private.Uri" />
|
<PackageReference Include="System.Private.Uri" />
|
||||||
<PackageReference Include="System.Text.RegularExpressions" />
|
<PackageReference Include="System.Text.RegularExpressions" />
|
||||||
|
|||||||
@@ -19,13 +19,20 @@
|
|||||||
H2Margin="0, 16, 0, 4"
|
H2Margin="0, 16, 0, 4"
|
||||||
H3FontSize="16"
|
H3FontSize="16"
|
||||||
H3FontWeight="SemiBold"
|
H3FontWeight="SemiBold"
|
||||||
H3Margin="0, 16, 0, 4" />
|
H3Margin="0, 16, 0, 4"
|
||||||
|
ImageMaxHeight="200"
|
||||||
|
ImageMaxWidth="600"
|
||||||
|
ImageStretch="Uniform" />
|
||||||
<tkcontrols:MarkdownConfig x:Key="ReleaseNotesMarkdownConfig" Themes="{StaticResource ReleaseNotesMarkdownThemeConfig}" />
|
<tkcontrols:MarkdownConfig x:Key="ReleaseNotesMarkdownConfig" Themes="{StaticResource ReleaseNotesMarkdownThemeConfig}" />
|
||||||
</Page.Resources>
|
</Page.Resources>
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Padding="24" Spacing="12">
|
<Grid Padding="24" RowSpacing="12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
<!-- Header image -->
|
<!-- Header image -->
|
||||||
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" CornerRadius="12">
|
<!--<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" CornerRadius="12">
|
||||||
<Image
|
<Image
|
||||||
x:Name="HeaderImage"
|
x:Name="HeaderImage"
|
||||||
Height="180"
|
Height="180"
|
||||||
@@ -33,16 +40,30 @@
|
|||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Stretch="UniformToFill"
|
Stretch="UniformToFill"
|
||||||
Visibility="Collapsed" />
|
Visibility="Collapsed" />
|
||||||
|
</Border>-->
|
||||||
|
<Border Width="1000">
|
||||||
|
<tkcontrols:MarkdownTextBlock
|
||||||
|
x:Name="BlogTextBlock"
|
||||||
|
MaxWidth="1000"
|
||||||
|
Config="{StaticResource ReleaseNotesMarkdownConfig}"
|
||||||
|
UseAutoLinks="True"
|
||||||
|
UseEmphasisExtras="True"
|
||||||
|
UseListExtras="True"
|
||||||
|
UsePipeTables="True"
|
||||||
|
UseTaskLists="True" />
|
||||||
|
|
||||||
|
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<tkcontrols:MarkdownTextBlock
|
<tkcontrols:MarkdownTextBlock
|
||||||
x:Name="MarkdownBlock"
|
x:Name="MarkdownBlock"
|
||||||
|
Grid.Row="1"
|
||||||
Config="{StaticResource ReleaseNotesMarkdownConfig}"
|
Config="{StaticResource ReleaseNotesMarkdownConfig}"
|
||||||
UseAutoLinks="True"
|
UseAutoLinks="True"
|
||||||
UseEmphasisExtras="True"
|
UseEmphasisExtras="True"
|
||||||
UseListExtras="True"
|
UseListExtras="True"
|
||||||
UsePipeTables="True"
|
UsePipeTables="True"
|
||||||
UseTaskLists="True" />
|
UseTaskLists="True" />
|
||||||
</StackPanel>
|
</Grid>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) Microsoft Corporation
|
// Copyright (c) Microsoft Corporation
|
||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
@@ -6,7 +6,10 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Runtime.InteropServices.WindowsRuntime;
|
using System.Runtime.InteropServices.WindowsRuntime;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
@@ -16,13 +19,19 @@ using Microsoft.UI.Xaml.Input;
|
|||||||
using Microsoft.UI.Xaml.Media;
|
using Microsoft.UI.Xaml.Media;
|
||||||
using Microsoft.UI.Xaml.Media.Imaging;
|
using Microsoft.UI.Xaml.Media.Imaging;
|
||||||
using Microsoft.UI.Xaml.Navigation;
|
using Microsoft.UI.Xaml.Navigation;
|
||||||
|
using ReverseMarkdown;
|
||||||
using Windows.Foundation;
|
using Windows.Foundation;
|
||||||
using Windows.Foundation.Collections;
|
using Windows.Foundation.Collections;
|
||||||
|
using Windows.System;
|
||||||
|
using static System.Net.WebRequestMethods;
|
||||||
|
using static System.Resources.ResXFileRef;
|
||||||
|
|
||||||
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||||
{
|
{
|
||||||
public sealed partial class ReleaseNotePage : Page
|
public sealed partial class ReleaseNotePage : Page
|
||||||
{
|
{
|
||||||
|
private static readonly HttpClient HttpClient = new();
|
||||||
|
|
||||||
public ReleaseNotePage()
|
public ReleaseNotePage()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -34,7 +43,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
|||||||
{
|
{
|
||||||
MarkdownBlock.Text = item.Markdown ?? string.Empty;
|
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))
|
Uri.TryCreate(item.HeaderImageUri, UriKind.Absolute, out var uri))
|
||||||
{
|
{
|
||||||
HeaderImage.Source = new BitmapImage(uri);
|
HeaderImage.Source = new BitmapImage(uri);
|
||||||
@@ -44,10 +53,168 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
|||||||
{
|
{
|
||||||
HeaderImage.Source = null;
|
HeaderImage.Source = null;
|
||||||
HeaderImage.Visibility = Visibility.Collapsed;
|
HeaderImage.Visibility = Visibility.Collapsed;
|
||||||
}
|
} */
|
||||||
|
|
||||||
|
GetBlogData();
|
||||||
}
|
}
|
||||||
|
|
||||||
base.OnNavigatedTo(e);
|
base.OnNavigatedTo(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string NormalizePreCodeToFences(string html)
|
||||||
|
{
|
||||||
|
// ```lang\ncode\n```
|
||||||
|
var rx = new Regex(
|
||||||
|
@"<pre[^>]*>\s*<code(?:(?:\s+class=""[^""]*language-([a-z0-9+\-]+)[^""]*"")|[^>]*)>([\s\S]*?)</code>\s*</pre>",
|
||||||
|
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 <pre>…</pre> without inner <code>
|
||||||
|
var rxPre = new Regex(@"<pre[^>]*>([\s\S]*?)</pre>", 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/<slug>/
|
||||||
|
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 <pre><code class="language-xxx">…</code></pre> 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, "(?<attr>(?:src|href))=(\"|')(?<url>[^\"']+)(\"|')",
|
||||||
|
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,
|
||||||
|
@"<img([^>]*?)>",
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user