-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathToolkitDocumentationRenderer.xaml.cs
More file actions
270 lines (229 loc) · 10.8 KB
/
ToolkitDocumentationRenderer.xaml.cs
File metadata and controls
270 lines (229 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Tooling.SampleGen;
using CommunityToolkit.Tooling.SampleGen.Metadata;
using Windows.Storage;
using Windows.System;
using static System.Net.WebRequestMethods;
#if !HAS_UNO
#if !WINAPPSDK
using Microsoft.Toolkit.Uwp.UI.Controls;
#else
using CommunityToolkit.WinUI.UI.Controls;
#endif
#endif
#nullable enable
namespace CommunityToolkit.App.Shared.Renderers;
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class ToolkitDocumentationRenderer : Page
{
private const string MarkdownRegexSampleTagExpression = @"^>\s*\[!SAMPLE\s*(?<sampleid>.*)\s*\]\s*$";
private static readonly Regex MarkdownRegexSampleTag = new Regex(MarkdownRegexSampleTagExpression, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);
private static string? ProjectUrl = null;
public ToolkitDocumentationRenderer()
{
// Check and Cache here before we use in XAML.
if (ProjectUrl == null)
{
ProjectUrl = Assembly.GetExecutingAssembly()?.GetCustomAttribute<CommunityToolkit.Attributes.PackageProjectUrlAttribute>()?.PackageProjectUrl;
}
this.InitializeComponent();
}
/// <summary>
/// List of referenced samples in this page.
/// </summary>
public List<ToolkitSampleMetadata> Samples
{
get { return (List<ToolkitSampleMetadata>)GetValue(SamplesProperty); }
set { SetValue(SamplesProperty, value); }
}
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="Samples"/> property.
/// </summary>
public static readonly DependencyProperty SamplesProperty
=
DependencyProperty.Register(nameof(Samples), typeof(List<ToolkitSampleMetadata>), typeof(ToolkitDocumentationRenderer), new PropertyMetadata(null));
/// <summary>
/// Intermixed list of string doc snippets for Markdown and <see cref="ToolkitSampleMetadata"/>
/// objects for samples.
/// </summary>
public ObservableCollection<object> DocsAndSamples = new();
/// <summary>
/// The YAML front matter metadata about this documentation file.
/// </summary>
public ToolkitFrontMatter? Metadata
{
get { return (ToolkitFrontMatter?)GetValue(MetadataProperty); }
set { SetValue(MetadataProperty, value); }
}
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="Metadata"/> property.
/// </summary>
public static readonly DependencyProperty MetadataProperty =
DependencyProperty.Register(nameof(Metadata), typeof(ToolkitFrontMatter), typeof(ToolkitDocumentationRenderer), new PropertyMetadata(null, OnMetadataPropertyChanged));
private static async void OnMetadataPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
if (dependencyObject is ToolkitDocumentationRenderer renderer &&
renderer.Metadata != null &&
args.OldValue != args.NewValue)
{
await renderer.LoadData();
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
Metadata = (ToolkitFrontMatter)e.Parameter;
}
private async Task LoadData()
{
if (Metadata is null)
{
return;
}
List<ToolkitSampleMetadata> samples = new();
if (Metadata.SampleIdReferences != null && Metadata.SampleIdReferences.Length > 0 &&
!string.IsNullOrWhiteSpace(Metadata.SampleIdReferences[0]))
{
foreach (var sampleid in Metadata.SampleIdReferences)
{
// We don't check here for key as we validate with SG.
samples.Add(ToolkitSampleRegistry.Listing[sampleid]);
}
}
Samples = samples;
var doctext = await GetDocumentationFileContents(Metadata);
var matches = MarkdownRegexSampleTag.Matches(doctext);
DocsAndSamples.Clear();
if (matches.Count == 0)
{
DocsAndSamples.Add(doctext);
}
else
{
int index = 0;
foreach (Match match in matches)
{
DocsAndSamples.Add(doctext.Substring(index, match.Index - index - 1));
DocsAndSamples.Add(ToolkitSampleRegistry.Listing[match.Groups["sampleid"].Value]);
index = match.Index + match.Length;
}
var rest = doctext.Substring(index).Trim();
// Put rest of text at end (if any)
if (rest.Length > 0)
{
DocsAndSamples.Add(rest);
}
}
}
private void SampleSelectionBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is ComboBox comboBox && comboBox.SelectedItem is ToolkitSampleMetadata metadata)
{
var container = DocItemsControl.ContainerFromItem(metadata) as UIElement;
container?.StartBringIntoView();
}
}
private static async Task<string> GetDocumentationFileContents(ToolkitFrontMatter metadata)
{
// TODO: https://github.com/CommunityToolkit/Labs-Windows/issues/142
// MSBuild uses wildcard to find the files, and the wildcards decide where they end up
// Single experiments use relative paths, the allExperiment head uses absolute paths that grab from all experiments
// The wildcard captures decide the paths. This discrepancy is accounted for manually.
// Logic here is the exact same that MSBuild uses to find and include the files we need.
var assemblyName = typeof(ToolkitSampleRenderer).Assembly.GetName().Name;
if (string.IsNullOrWhiteSpace(assemblyName))
throw new InvalidOperationException();
var isAllExperimentHead = assemblyName.StartsWith("CommunityToolkit.", StringComparison.OrdinalIgnoreCase);
var isProjectTemplateHead = assemblyName.StartsWith("ProjectTemplate");
var isSingleExperimentHead = !isAllExperimentHead && !isProjectTemplateHead;
if (metadata.FilePath is null || string.IsNullOrWhiteSpace(metadata.FilePath))
throw new InvalidOperationException("Missing or malformed path to markdown file. Unable to continue;");
// Normalize the path separators
var path = metadata.FilePath;
var fileUri = new Uri($"ms-appx:///SourceAssets/{(isSingleExperimentHead ? Path.GetFileName(path.Replace('\\', '/')) : path)}");
try
{
var file = await StorageFile.GetFileFromApplicationUriAsync(fileUri);
var textContents = await FileIO.ReadTextAsync(file);
// Remove YAML - need to use array overload as single string not supported on .NET Standard 2.0
var blocks = textContents.Split(new[] { "---" }, 2, StringSplitOptions.RemoveEmptyEntries);
return blocks.LastOrDefault()?.Replace("<br>", " ") ?? "Couldn't find content after YAML Front Matter removal.";
}
catch (Exception e)
{
return $"Exception Encountered Loading file '{fileUri}':\n{e.Message}\n{e.StackTrace}";
}
}
#if HAS_UNO
private void MarkdownTextBlock_LinkClicked(object sender, LinkClickedEventArgs e)
{
// No-op - WASM handles via browser 'a' tag, Windows has handler below.
// TODO: For other platforms
}
#elif !HAS_UNO
private async void MarkdownTextBlock_LinkClicked(object sender, LinkClickedEventArgs e)
{
if (!Uri.IsWellFormedUriString(e.Link, UriKind.Absolute))
{
await new ContentDialog
{
Title = "Windows Community Toolkit Labs Sample App",
Content = $"Link {e.Link} was malformed.",
CloseButtonText = "Close",
XamlRoot = XamlRoot // TODO: For UWP this is only on 1903+
}.ShowAsync();
}
else
{
await Launcher.LaunchUriAsync(new Uri(e.Link));
}
}
#endif
public static Uri? ToGitHubUri(string path, int id) => IsProjectPathValid() ? new Uri($"{ProjectUrl}/{path}/{id}") : null;
public static Uri? ToComponentUri(string name, bool? isExperimental = null)
{
if (IsProjectPathValid() is not true)
{
return null;
}
string? url = (isExperimental is null || isExperimental is false)
? ProjectUrl
: ProjectUrl?.Replace("Windows", "Labs-Windows");
return new Uri($"{url}/tree/main/components/{name}");
}
public static Uri? ToPackageUri(string platform, string projectFileName, bool? isExperimental = null)
{
if (isExperimental is null || isExperimental is false)
{
return new Uri($"https://www.nuget.org/packages/{ToPackageName(platform, projectFileName, isExperimental)}");
}
else
{
// Labs feed for experimental packages (currently)
// See inconsistency for Labs package names/project names https://github.com/CommunityToolkit/Windows/issues/587#issuecomment-2738529086
return new Uri($"https://dev.azure.com/dotnet/CommunityToolkit/_artifacts/feed/CommunityToolkit-Labs/NuGet/{ToPackageName(platform, projectFileName, isExperimental)}");
}
}
public static string ToPackageName(string platform, string projectFileName, bool? isExperimental) => RemoveFileExtension(projectFileName).Replace("CommunityToolkit.WinUI", isExperimental == true ? "CommunityToolkit.Labs.WinUI" : "CommunityToolkit.WinUI").Replace("WinUI", platform);
// TODO: Think this is most of the special cases with Controls and the Extensions/Triggers using the base namespace
// See: https://github.com/CommunityToolkit/Tooling-Windows-Submodule/issues/105#issuecomment-1698306420
// And: https://github.com/unoplatform/uno/issues/8750 - otherwise we could use csproj data and inject with SG.
public static string ToPackageNamespace(string projectFileName) => RemoveFileExtension(projectFileName) switch
{
string c when c.Contains("Controls") => "CommunityToolkit.WinUI.Controls",
string e when e.Contains("Extensions") || e.Contains("Triggers") => "CommunityToolkit.WinUI",
_ => RemoveFileExtension(projectFileName)
};
private static string RemoveFileExtension(string filename) => filename.Replace(".csproj", "");
public static Visibility IsIdValid(int id) => id switch
{
<= 0 => Visibility.Collapsed,
_ => Visibility.Visible,
};
public static bool IsProjectPathValid() => !string.IsNullOrWhiteSpace(ProjectUrl);
private bool BoolFalseIfNull(bool? value) => value ?? false;
}