From d2261245a5d9dacb665af2875765944e4758899e Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Mon, 28 Aug 2023 16:54:12 -0700 Subject: [PATCH 1/5] Validate Document Registry Generation Fix issue with Author missing Fix issue with old path setup for markdown file path --- .../Helpers/TestHelpers.Compilation.cs | 2 +- ...oolkitSampleMetadataTests.Documentation.cs | 49 ++++++++++++++++++- ...itSampleMetadataGenerator.Documentation.cs | 5 ++ .../ToolkitSampleMetadataGenerator.Sample.cs | 2 +- 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.Compilation.cs b/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.Compilation.cs index 5d309c28..67435426 100644 --- a/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.Compilation.cs +++ b/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.Compilation.cs @@ -50,7 +50,7 @@ internal static GeneratorDriver WithMarkdown(this GeneratorDriver driver, params { if (!string.IsNullOrWhiteSpace(markdown)) { - var text = new InMemoryAdditionalText(@"C:\pathtorepo\components\experiment\samples\experiment.Samples\documentation.md", markdown); + var text = new InMemoryAdditionalText(@"C:\pathtorepo\components\experiment\samples\documentation.md", markdown); driver = driver.AddAdditionalTexts(ImmutableArray.Create(text)); } } diff --git a/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs b/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs index 3d496c2a..1ba9db40 100644 --- a/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs +++ b/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs @@ -141,7 +141,7 @@ public void DocumentationMissingSample() } [TestMethod] - public void DocumentationValid() + public void DocumentationValidNoDiagnostics() { string markdown = @"--- title: Canvas Layout @@ -216,4 +216,51 @@ public void DocumentationInvalidIssueId() result.AssertNoCompilationErrors(); result.AssertDiagnosticsAre(DiagnosticDescriptors.MarkdownYAMLFrontMatterException, DiagnosticDescriptors.DocumentationHasNoSamples); } + + [TestMethod] + public void DocumentationValidWithRegistry() + { + string markdown = @"--- +title: Canvas Layout +author: mhawker +description: A canvas-like VirtualizingLayout for use in an ItemsRepeater +keywords: CanvasLayout, ItemsRepeater, VirtualizingLayout, Canvas, Layout, Panel, Arrange +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 0 +issue-id: 0 +icon: assets/icon.png +--- +# This is some test documentation... +Which is valid. +> [!SAMPLE Sample]"; + + var sampleProjectAssembly = SimpleSource.ToSyntaxTree() + .CreateCompilation("MyApp.Samples") + .ToMetadataReference(); + + var headCompilation = string.Empty + .ToSyntaxTree() + .CreateCompilation("MyApp.Head") + .AddReferences(sampleProjectAssembly); + + var result = headCompilation.RunSourceGenerator(markdown); + + result.AssertNoCompilationErrors(); + + Assert.AreEqual(result.Compilation.GetFileContentsByName("ToolkitDocumentRegistry.g.cs"), """ + #nullable enable + namespace CommunityToolkit.Tooling.SampleGen; + + public static class ToolkitDocumentRegistry + { + public static System.Collections.Generic.IEnumerable Execute() + { + yield return new CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitFrontMatter() { Title = "Canvas Layout", Author = "mhawker", Description = "A canvas-like VirtualizingLayout for use in an ItemsRepeater", Keywords = "CanvasLayout, ItemsRepeater, VirtualizingLayout, Canvas, Layout, Panel, Arrange", Category = ToolkitSampleCategory.Controls, Subcategory = ToolkitSampleSubcategory.Layout, DiscussionId = 0, IssueId = 0, Icon = @"experiment/samples/assets/icon.png", FilePath = @"experiment\samples\documentation.md", SampleIdReferences = new string[] { "Sample" } }; + } + } + """, "Unexpected code generated"); + } } diff --git a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Documentation.cs b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Documentation.cs index eec3be7b..b4698b07 100644 --- a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Documentation.cs +++ b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Documentation.cs @@ -19,6 +19,9 @@ public partial class ToolkitSampleMetadataGenerator private const string FrontMatterRegexTitleExpression = @"^title:\s*(?.*)$"; private static readonly Regex FrontMatterRegexTitle = new Regex(FrontMatterRegexTitleExpression, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline); + private const string FrontMatterRegexAuthorExpression = @"^author:\s*(?<author>.*)$"; + private static readonly Regex FrontMatterRegexAuthor = new Regex(FrontMatterRegexAuthorExpression, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline); + private const string FrontMatterRegexDescriptionExpression = @"^description:\s*(?<description>.*)$"; private static readonly Regex FrontMatterRegexDescription = new Regex(FrontMatterRegexDescriptionExpression, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline); @@ -110,6 +113,7 @@ private ImmutableArray<ToolkitFrontMatter> GatherDocumentFrontMatter(SourceProdu // Grab all front matter fields using RegEx expressions. var title = ParseYamlField(ref ctx, file.Path, ref frontmatter, FrontMatterRegexTitle, "title"); + var author = ParseYamlField(ref ctx, file.Path, ref frontmatter, FrontMatterRegexAuthor, "author"); var description = ParseYamlField(ref ctx, file.Path, ref frontmatter, FrontMatterRegexDescription, "description"); var keywords = ParseYamlField(ref ctx, file.Path, ref frontmatter, FrontMatterRegexKeywords, "keywords"); @@ -204,6 +208,7 @@ private ImmutableArray<ToolkitFrontMatter> GatherDocumentFrontMatter(SourceProdu return new ToolkitFrontMatter() { Title = title!, + Author = author!, Description = description!, Keywords = keywords!, Category = categoryValue, diff --git a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Sample.cs b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Sample.cs index cdf49a76..9c052787 100644 --- a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Sample.cs +++ b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Sample.cs @@ -38,7 +38,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var assemblyName = context.CompilationProvider.Select((x, _) => x.Assembly.Name); // Only generate diagnostics (sample projects) - // Skip creating the registry for symbols in the executing assembly. This would place an incomplete registry in each sample project and cause compiler erorrs. + // Skip creating the registry for symbols in the executing assembly. This would place an incomplete registry in each sample project and cause compiler errors. Execute(symbolsInExecutingAssembly, skipRegistry: true); // Only generate the registry (project head) From 267452510c92f5bff9a8fd8f91d2ffb35d9311bf Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 29 Aug 2023 11:41:46 -0700 Subject: [PATCH 2/5] Add `experimental` frontmatter value to doc source generator Can be used for Labs to mark experimental components when we interleave things together. --- ...oolkitSampleMetadataTests.Documentation.cs | 47 ++++++++++++++++--- .../Metadata/ToolkitFrontMatter.cs | 1 + ...itSampleMetadataGenerator.Documentation.cs | 28 +++++++++-- 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs b/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs index 1ba9db40..9642867e 100644 --- a/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs +++ b/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs @@ -2,12 +2,9 @@ // 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.Attributes; using CommunityToolkit.Tooling.SampleGen.Diagnostics; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.VisualStudio.TestTools.UnitTesting; using CommunityToolkit.Tooling.SampleGen.Tests.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CommunityToolkit.Tooling.SampleGen.Tests; @@ -184,12 +181,15 @@ public void DocumentationInvalidDiscussionId() icon: assets/icon.png --- # This is some test documentation... -Without an invalid discussion id."; +Without an invalid discussion-id."; var result = string.Empty.RunSourceGenerator<ToolkitSampleMetadataGenerator>(SAMPLE_ASM_NAME, markdown); result.AssertNoCompilationErrors(); result.AssertDiagnosticsAre(DiagnosticDescriptors.MarkdownYAMLFrontMatterException, DiagnosticDescriptors.DocumentationHasNoSamples); + + var diag = result.Diagnostics.First((d) => d.Descriptor == DiagnosticDescriptors.MarkdownYAMLFrontMatterException); + Assert.IsTrue(diag.GetMessage().Contains("discussion-id")); } [TestMethod] @@ -209,12 +209,44 @@ public void DocumentationInvalidIssueId() icon: assets/icon.png --- # This is some test documentation... -Without an invalid discussion id."; +Without an invalid issue-id."; + + var result = string.Empty.RunSourceGenerator<ToolkitSampleMetadataGenerator>(SAMPLE_ASM_NAME, markdown); + + result.AssertNoCompilationErrors(); + result.AssertDiagnosticsAre(DiagnosticDescriptors.MarkdownYAMLFrontMatterException, DiagnosticDescriptors.DocumentationHasNoSamples); + + var diag = result.Diagnostics.First((d) => d.Descriptor == DiagnosticDescriptors.MarkdownYAMLFrontMatterException); + Assert.IsTrue(diag.GetMessage().Contains("issue-id")); + } + + [TestMethod] + public void DocumentationInvalidIsExperimental() + { + string markdown = @"--- +title: Canvas Layout +author: mhawker +description: A canvas-like VirtualizingLayout for use in an ItemsRepeater +keywords: CanvasLayout, ItemsRepeater, VirtualizingLayout, Canvas, Layout, Panel, Arrange +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 0 +issue-id: 0 +icon: assets/icon.png +experimental: No +--- +# This is some test documentation... +Without an invalid experimental value."; var result = string.Empty.RunSourceGenerator<ToolkitSampleMetadataGenerator>(SAMPLE_ASM_NAME, markdown); result.AssertNoCompilationErrors(); result.AssertDiagnosticsAre(DiagnosticDescriptors.MarkdownYAMLFrontMatterException, DiagnosticDescriptors.DocumentationHasNoSamples); + + var diag = result.Diagnostics.First((d) => d.Descriptor == DiagnosticDescriptors.MarkdownYAMLFrontMatterException); + Assert.IsTrue(diag.GetMessage().Contains("experimental")); } [TestMethod] @@ -232,6 +264,7 @@ public void DocumentationValidWithRegistry() discussion-id: 0 issue-id: 0 icon: assets/icon.png +experimental: true --- # This is some test documentation... Which is valid. @@ -258,7 +291,7 @@ public static class ToolkitDocumentRegistry { public static System.Collections.Generic.IEnumerable<CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitFrontMatter> Execute() { - yield return new CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitFrontMatter() { Title = "Canvas Layout", Author = "mhawker", Description = "A canvas-like VirtualizingLayout for use in an ItemsRepeater", Keywords = "CanvasLayout, ItemsRepeater, VirtualizingLayout, Canvas, Layout, Panel, Arrange", Category = ToolkitSampleCategory.Controls, Subcategory = ToolkitSampleSubcategory.Layout, DiscussionId = 0, IssueId = 0, Icon = @"experiment/samples/assets/icon.png", FilePath = @"experiment\samples\documentation.md", SampleIdReferences = new string[] { "Sample" } }; + yield return new CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitFrontMatter() { Title = "Canvas Layout", Author = "mhawker", Description = "A canvas-like VirtualizingLayout for use in an ItemsRepeater", Keywords = "CanvasLayout, ItemsRepeater, VirtualizingLayout, Canvas, Layout, Panel, Arrange", Category = ToolkitSampleCategory.Controls, Subcategory = ToolkitSampleSubcategory.Layout, DiscussionId = 0, IssueId = 0, Icon = @"experiment/samples/assets/icon.png", FilePath = @"experiment\samples\documentation.md", SampleIdReferences = new string[] { "Sample" }, IsExperimental = true }; } } """, "Unexpected code generated"); diff --git a/CommunityToolkit.Tooling.SampleGen/Metadata/ToolkitFrontMatter.cs b/CommunityToolkit.Tooling.SampleGen/Metadata/ToolkitFrontMatter.cs index bc7bef91..296ed6a3 100644 --- a/CommunityToolkit.Tooling.SampleGen/Metadata/ToolkitFrontMatter.cs +++ b/CommunityToolkit.Tooling.SampleGen/Metadata/ToolkitFrontMatter.cs @@ -19,6 +19,7 @@ public sealed class ToolkitFrontMatter : DocsFrontMatter public ToolkitSampleSubcategory Subcategory { get; set; } public int DiscussionId { get; set; } public int IssueId { get; set; } + public bool? IsExperimental { get; set; } //// Extra Metadata needed for Sample App public string? FilePath { get; set; } diff --git a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Documentation.cs b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Documentation.cs index b4698b07..5369ecff 100644 --- a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Documentation.cs +++ b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Documentation.cs @@ -46,6 +46,9 @@ public partial class ToolkitSampleMetadataGenerator 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 const string FrontMatterRegexIsExperimentalExpression = @"^experimental:\s*(?<experimental>.*)$"; + private static readonly Regex FrontMatterRegexIsExperimental = new Regex(FrontMatterRegexIsExperimentalExpression, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline); + private static void ReportDocumentDiagnostics(SourceProductionContext ctx, Dictionary<string, ToolkitSampleRecord> sampleMetadata, IEnumerable<AdditionalText> markdownFileData, IEnumerable<(ToolkitSampleAttribute Attribute, string AttachedQualifiedTypeName, ISymbol Symbol)> toolkitSampleAttributeData, ImmutableArray<ToolkitFrontMatter> docFrontMatter) { // Keep track of all sample ids and remove them as we reference them so we know if we have any unreferenced samples. @@ -126,6 +129,8 @@ private ImmutableArray<ToolkitFrontMatter> GatherDocumentFrontMatter(SourceProdu var discussion = ParseYamlField(ref ctx, file.Path, ref frontmatter, FrontMatterRegexDiscussionId, "discussionid")?.Trim(); var issue = ParseYamlField(ref ctx, file.Path, ref frontmatter, FrontMatterRegexIssueId, "issueid")?.Trim(); + var experimental = ParseYamlField(ref ctx, file.Path, ref frontmatter, FrontMatterRegexIsExperimental, "experimental", true)?.Trim(); + // Check we have all the fields we expect to continue (errors will have been spit out otherwise already from the ParseYamlField method) if (title == null || description == null || keywords == null || category == null || subcategory == null || discussion == null || issue == null || icon == null) @@ -204,6 +209,18 @@ private ImmutableArray<ToolkitFrontMatter> GatherDocumentFrontMatter(SourceProdu return null; } + bool isExperimental = false; + if (experimental != null && !bool.TryParse(experimental, out isExperimental)) + { + ctx.ReportDiagnostic( + Diagnostic.Create( + DiagnosticDescriptors.MarkdownYAMLFrontMatterException, + Location.Create(file.Path, TextSpan.FromBounds(0, 1), new LinePositionSpan(LinePosition.Zero, LinePosition.Zero)), + file.Path, + "Can't parse optional experimental field, must be a boolean value like 'true' or 'false' or remove it.")); + return null; + } + // Finally, construct the complete object. return new ToolkitFrontMatter() { @@ -218,16 +235,17 @@ private ImmutableArray<ToolkitFrontMatter> GatherDocumentFrontMatter(SourceProdu DiscussionId = discussionId, IssueId = issueId, Icon = iconpath, + IsExperimental = isExperimental, }; } }).OfType<ToolkitFrontMatter>().ToImmutableArray(); } - private string? ParseYamlField(ref SourceProductionContext ctx, string filepath, ref string content, Regex pattern, string captureGroupName) + private string? ParseYamlField(ref SourceProductionContext ctx, string filepath, ref string content, Regex pattern, string captureGroupName, bool optional = false) { var match = pattern.Match(content); - if (!match.Success) + if (!optional && !match.Success) { ctx.ReportDiagnostic( Diagnostic.Create( @@ -237,6 +255,10 @@ private ImmutableArray<ToolkitFrontMatter> GatherDocumentFrontMatter(SourceProdu captureGroupName)); return null; } + else if (optional && !match.Success) + { + return null; + } return match.Groups[captureGroupName].Value.Trim(); } @@ -270,6 +292,6 @@ private static string FrontMatterToRegistryCall(ToolkitFrontMatter metadata) var categoryParam = $"{nameof(ToolkitSampleCategory)}.{metadata.Category}"; var subcategoryParam = $"{nameof(ToolkitSampleSubcategory)}.{metadata.Subcategory}"; - return @$"yield return new {typeof(ToolkitFrontMatter).FullName}() {{ Title = ""{metadata.Title}"", Author = ""{metadata.Author}"", Description = ""{metadata.Description}"", Keywords = ""{metadata.Keywords}"", Category = {categoryParam}, Subcategory = {subcategoryParam}, DiscussionId = {metadata.DiscussionId}, IssueId = {metadata.IssueId}, Icon = @""{metadata.Icon}"", FilePath = @""{metadata.FilePath}"", SampleIdReferences = new string[] {{ ""{string.Join("\",\"", metadata.SampleIdReferences)}"" }} }};"; // TODO: Add list of sample ids in document + return @$"yield return new {typeof(ToolkitFrontMatter).FullName}() {{ Title = ""{metadata.Title}"", Author = ""{metadata.Author}"", Description = ""{metadata.Description}"", Keywords = ""{metadata.Keywords}"", Category = {categoryParam}, Subcategory = {subcategoryParam}, DiscussionId = {metadata.DiscussionId}, IssueId = {metadata.IssueId}, Icon = @""{metadata.Icon}"", FilePath = @""{metadata.FilePath}"", SampleIdReferences = new string[] {{ ""{string.Join("\",\"", metadata.SampleIdReferences)}"" }}, IsExperimental = {metadata.IsExperimental.ToString().ToLowerInvariant()} }};"; // TODO: Add list of sample ids in document } } From e519798dbcca3dedd95dd5e105169ed5e1495aea Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 29 Aug 2023 16:55:24 -0700 Subject: [PATCH 3/5] Add csproj file support to Documentation Source Generator This will allow us to pass them into the generator and get additional info we can use in the Sample App for pointing folks towards Source code and Packages (for now). --- .../Helpers/TestHelpers.Compilation.cs | 14 +++++++ .../Helpers/TestHelpers.cs | 5 ++- ...oolkitSampleMetadataTests.Documentation.cs | 12 +++++- .../Metadata/ToolkitFrontMatter.cs | 2 + ...itSampleMetadataGenerator.Documentation.cs | 41 ++++++++++++++++--- .../ToolkitSampleMetadataGenerator.Sample.cs | 36 ++++++++++++---- 6 files changed, 92 insertions(+), 18 deletions(-) diff --git a/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.Compilation.cs b/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.Compilation.cs index 67435426..b19d6854 100644 --- a/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.Compilation.cs +++ b/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.Compilation.cs @@ -57,4 +57,18 @@ internal static GeneratorDriver WithMarkdown(this GeneratorDriver driver, params return driver; } + + internal static GeneratorDriver WithCsproj(this GeneratorDriver driver, params string[] csprojFilesToCreate) + { + foreach (var proj in csprojFilesToCreate) + { + if (!string.IsNullOrWhiteSpace(proj)) + { + var text = new InMemoryAdditionalText(@"C:\pathtorepo\components\experiment\src\componentname.csproj", proj); + driver = driver.AddAdditionalTexts(ImmutableArray.Create<AdditionalText>(text)); + } + } + + return driver; + } } diff --git a/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.cs b/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.cs index c7c542d1..dc94b73a 100644 --- a/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.cs +++ b/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.cs @@ -19,13 +19,14 @@ internal static SourceGeneratorRunResult RunSourceGenerator<TGenerator>(this Syn return RunSourceGenerator<TGenerator>(compilation, markdown); } - internal static SourceGeneratorRunResult RunSourceGenerator<TGenerator>(this Compilation compilation, string markdown = "") + internal static SourceGeneratorRunResult RunSourceGenerator<TGenerator>(this Compilation compilation, string markdown = "", string csproj = "") where TGenerator : class, IIncrementalGenerator, new() { // Create a driver for the source generator var driver = compilation .CreateSourceGeneratorDriver(new TGenerator()) - .WithMarkdown(markdown); + .WithMarkdown(markdown) + .WithCsproj(csproj); // Update the original compilation using the source generator _ = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation generatorCompilation, out ImmutableArray<Diagnostic> postGeneratorCompilationDiagnostics); diff --git a/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs b/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs index 9642867e..9b52abb8 100644 --- a/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs +++ b/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs @@ -270,6 +270,14 @@ public void DocumentationValidWithRegistry() Which is valid. > [!SAMPLE Sample]"; + string csproj = """ +<Project Sdk="MSBuild.Sdk.Extras/3.0.23"> + <PropertyGroup> + <ToolkitComponentName>Primitives</ToolkitComponentName> + </PropertyGroup> +</Project> +"""; + var sampleProjectAssembly = SimpleSource.ToSyntaxTree() .CreateCompilation("MyApp.Samples") .ToMetadataReference(); @@ -279,7 +287,7 @@ Which is valid. .CreateCompilation("MyApp.Head") .AddReferences(sampleProjectAssembly); - var result = headCompilation.RunSourceGenerator<ToolkitSampleMetadataGenerator>(markdown); + var result = headCompilation.RunSourceGenerator<ToolkitSampleMetadataGenerator>(markdown, csproj); result.AssertNoCompilationErrors(); @@ -291,7 +299,7 @@ public static class ToolkitDocumentRegistry { public static System.Collections.Generic.IEnumerable<CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitFrontMatter> Execute() { - yield return new CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitFrontMatter() { Title = "Canvas Layout", Author = "mhawker", Description = "A canvas-like VirtualizingLayout for use in an ItemsRepeater", Keywords = "CanvasLayout, ItemsRepeater, VirtualizingLayout, Canvas, Layout, Panel, Arrange", Category = ToolkitSampleCategory.Controls, Subcategory = ToolkitSampleSubcategory.Layout, DiscussionId = 0, IssueId = 0, Icon = @"experiment/samples/assets/icon.png", FilePath = @"experiment\samples\documentation.md", SampleIdReferences = new string[] { "Sample" }, IsExperimental = true }; + yield return new CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitFrontMatter() { ComponentName = "Primitives", Title = "Canvas Layout", Author = "mhawker", Description = "A canvas-like VirtualizingLayout for use in an ItemsRepeater", Keywords = "CanvasLayout, ItemsRepeater, VirtualizingLayout, Canvas, Layout, Panel, Arrange", Category = ToolkitSampleCategory.Controls, Subcategory = ToolkitSampleSubcategory.Layout, DiscussionId = 0, IssueId = 0, Icon = @"experiment/samples/assets/icon.png", FilePath = @"experiment\samples\documentation.md", SampleIdReferences = new string[] { "Sample" }, IsExperimental = true, CsProjName = @"componentname.csproj" }; } } """, "Unexpected code generated"); diff --git a/CommunityToolkit.Tooling.SampleGen/Metadata/ToolkitFrontMatter.cs b/CommunityToolkit.Tooling.SampleGen/Metadata/ToolkitFrontMatter.cs index 296ed6a3..73981fde 100644 --- a/CommunityToolkit.Tooling.SampleGen/Metadata/ToolkitFrontMatter.cs +++ b/CommunityToolkit.Tooling.SampleGen/Metadata/ToolkitFrontMatter.cs @@ -25,4 +25,6 @@ public sealed class ToolkitFrontMatter : DocsFrontMatter public string? FilePath { get; set; } public string[]? SampleIdReferences { get; set; } public string? Icon { get; set; } + public string? ComponentName { get; set; } + public string? CsProjName { get; set; } } diff --git a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Documentation.cs b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Documentation.cs index 5369ecff..53f4f99a 100644 --- a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Documentation.cs +++ b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Documentation.cs @@ -49,6 +49,9 @@ public partial class ToolkitSampleMetadataGenerator private const string FrontMatterRegexIsExperimentalExpression = @"^experimental:\s*(?<experimental>.*)$"; private static readonly Regex FrontMatterRegexIsExperimental = new Regex(FrontMatterRegexIsExperimentalExpression, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline); + private const string CsProjRegexComponentNameExpression = @"^\s*<ToolkitComponentName>(?<ToolkitComponentName>.*)<\/ToolkitComponentName>\s*$"; + private static readonly Regex CsProjRegexComponentName = new Regex(CsProjRegexComponentNameExpression, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline); + private static void ReportDocumentDiagnostics(SourceProductionContext ctx, Dictionary<string, ToolkitSampleRecord> sampleMetadata, IEnumerable<AdditionalText> markdownFileData, IEnumerable<(ToolkitSampleAttribute Attribute, string AttachedQualifiedTypeName, ISymbol Symbol)> toolkitSampleAttributeData, ImmutableArray<ToolkitFrontMatter> docFrontMatter) { // Keep track of all sample ids and remove them as we reference them so we know if we have any unreferenced samples. @@ -90,14 +93,16 @@ private static void ReportDocumentDiagnostics(SourceProductionContext ctx, Dicti } } - private ImmutableArray<ToolkitFrontMatter> GatherDocumentFrontMatter(SourceProductionContext ctx, IEnumerable<AdditionalText> data) + private static ImmutableArray<ToolkitFrontMatter> GatherDocumentFrontMatter(SourceProductionContext ctx, IEnumerable<(AdditionalText Document, AdditionalText? CsProj)> data) { - return data.Select(file => + return data.Select(info => { + var file = info.Document; + // We have to manually parse the YAML here for now because of // https://github.com/dotnet/roslyn/issues/43903 - var content = file.GetText()!.ToString(); + var content = info.Document.GetText()!.ToString(); var matter = content.Split(new[] { "---" }, StringSplitOptions.RemoveEmptyEntries); if (matter.Length <= 1) @@ -221,6 +226,28 @@ private ImmutableArray<ToolkitFrontMatter> GatherDocumentFrontMatter(SourceProdu return null; } + string? componentName = null; + string? csprojName = null; + + // Get component name from csproj file (if we have one) and its filename, otherwise, use path data of doc file + if (info.CsProj != null) + { + var text = info.CsProj.GetText()!.ToString(); + + var match = CsProjRegexComponentName.Match(text); + + if (match.Success) + { + componentName = match.Groups["ToolkitComponentName"].Value.Trim(); + } + + csprojName = info.CsProj.Path.Split(new char[] { '/', '\\' }).LastOrDefault(); + } + else + { + componentName = filepath.Split(new char[] { '/', '\\' }).FirstOrDefault(); + } + // Finally, construct the complete object. return new ToolkitFrontMatter() { @@ -236,12 +263,14 @@ private ImmutableArray<ToolkitFrontMatter> GatherDocumentFrontMatter(SourceProdu IssueId = issueId, Icon = iconpath, IsExperimental = isExperimental, + ComponentName = componentName, + CsProjName = csprojName, }; } }).OfType<ToolkitFrontMatter>().ToImmutableArray(); } - private string? ParseYamlField(ref SourceProductionContext ctx, string filepath, ref string content, Regex pattern, string captureGroupName, bool optional = false) + private static string? ParseYamlField(ref SourceProductionContext ctx, string filepath, ref string content, Regex pattern, string captureGroupName, bool optional = false) { var match = pattern.Match(content); @@ -263,7 +292,7 @@ private ImmutableArray<ToolkitFrontMatter> GatherDocumentFrontMatter(SourceProdu return match.Groups[captureGroupName].Value.Trim(); } - private void CreateDocumentRegistry(SourceProductionContext ctx, ImmutableArray<ToolkitFrontMatter> matter) + private static void CreateDocumentRegistry(SourceProductionContext ctx, ImmutableArray<ToolkitFrontMatter> matter) { // TODO: Emit a better error that no documentation is here? if (matter.Length == 0) @@ -292,6 +321,6 @@ private static string FrontMatterToRegistryCall(ToolkitFrontMatter metadata) var categoryParam = $"{nameof(ToolkitSampleCategory)}.{metadata.Category}"; var subcategoryParam = $"{nameof(ToolkitSampleSubcategory)}.{metadata.Subcategory}"; - return @$"yield return new {typeof(ToolkitFrontMatter).FullName}() {{ Title = ""{metadata.Title}"", Author = ""{metadata.Author}"", Description = ""{metadata.Description}"", Keywords = ""{metadata.Keywords}"", Category = {categoryParam}, Subcategory = {subcategoryParam}, DiscussionId = {metadata.DiscussionId}, IssueId = {metadata.IssueId}, Icon = @""{metadata.Icon}"", FilePath = @""{metadata.FilePath}"", SampleIdReferences = new string[] {{ ""{string.Join("\",\"", metadata.SampleIdReferences)}"" }}, IsExperimental = {metadata.IsExperimental.ToString().ToLowerInvariant()} }};"; // TODO: Add list of sample ids in document + return @$"yield return new {typeof(ToolkitFrontMatter).FullName}() {{ ComponentName = ""{metadata.ComponentName}"", Title = ""{metadata.Title}"", Author = ""{metadata.Author}"", Description = ""{metadata.Description}"", Keywords = ""{metadata.Keywords}"", Category = {categoryParam}, Subcategory = {subcategoryParam}, DiscussionId = {metadata.DiscussionId}, IssueId = {metadata.IssueId}, Icon = @""{metadata.Icon}"", FilePath = @""{metadata.FilePath}"", SampleIdReferences = new string[] {{ ""{string.Join("\",\"", metadata.SampleIdReferences)}"" }}, IsExperimental = {metadata.IsExperimental.ToString().ToLowerInvariant()}, CsProjName = @""{metadata.CsProjName}"" }};"; // TODO: Add list of sample ids in document } } diff --git a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Sample.cs b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Sample.cs index 9c052787..c452936a 100644 --- a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Sample.cs +++ b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Sample.cs @@ -8,6 +8,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Linq; namespace CommunityToolkit.Tooling.SampleGen; @@ -35,6 +36,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Where(static file => file.Path.EndsWith(".md")) .Collect(); + var csprojFiles = context.AdditionalTextsProvider + .Where(static file => file.Path.EndsWith(".csproj")) + .Collect(); + var assemblyName = context.CompilationProvider.Select((x, _) => x.Assembly.Name); // Only generate diagnostics (sample projects) @@ -106,15 +111,30 @@ void Execute(IncrementalValuesProvider<ISymbol> types, bool skipDiagnostics = fa .Combine(toolkitSampleAttributeData) .Combine(generatedPaneOptions) .Combine(markdownFiles) + .Combine(csprojFiles) .Combine(assemblyName); + // TODO: We can make this static if we could pass in our two boolean values as context, no idea how to do that... context.RegisterSourceOutput(all, (ctx, data) => { - var toolkitSampleAttributeData = data.Left.Left.Left.Right.Where(x => x != default).Distinct(); - var optionsPaneAttribute = data.Left.Left.Left.Left.Where(x => x != default).Distinct(); - var generatedOptionPropertyData = data.Left.Left.Right.Where(x => x.Attribute is not null && x.Symbol is not null); - var markdownFileData = data.Left.Right.Where(x => x != default).Distinct(); - var currentAssembly = data.Right; + var (((((optionsPaneAttributes, toolkitSampleAttributes), generatedPaneOptions), markdownFiles), csprojFiles), currentAssembly) = data; + + var toolkitSampleAttributeData = toolkitSampleAttributes.Where(x => x != default).Distinct(); + var optionsPaneAttributeData = optionsPaneAttributes.Where(x => x != default).Distinct(); + var generatedOptionPropertyData = generatedPaneOptions.Where(x => x.Attribute is not null && x.Symbol is not null); + + var markdownFileData = markdownFiles.Where(x => x != default).Distinct(); + var csprojFileData = csprojFiles.Where(x => x != default).Distinct(); + + var markdownProjPairings = markdownFileData.Select<AdditionalText, (AdditionalText Document, AdditionalText? CsProj)>((docFile, _) => + { + // TODO: We use these splits a lot to extra path info, so we should probably make a helper function? + var rootPathFile = docFile.Path.Split(new string[] { @"\components\", "/components/", @"\tooling\", "/tooling/" }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault()?.Split(new char[] { '/', '\\' }).FirstOrDefault(); + + var csproj = csprojFileData.FirstOrDefault(csProjFile => csProjFile.Path.Split(new string[] { @"\components\", "/components/", @"\tooling\", "/tooling/" }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault()?.Split(new char[] { '/', '\\' }).FirstOrDefault() == rootPathFile); + + return (docFile, csproj); + }); var isExecutingInSampleProject = currentAssembly?.EndsWith(".Samples") ?? false; @@ -129,16 +149,16 @@ void Execute(IncrementalValuesProvider<ISymbol> types, bool skipDiagnostics = fa sample.Attribute.DisplayName, sample.Attribute.Description, sample.AttachedQualifiedTypeName, - optionsPaneAttribute.FirstOrDefault(x => x.Item1?.SampleId == sample.Attribute.Id).Item2?.ToString(), + optionsPaneAttributeData.FirstOrDefault(x => x.Item1?.SampleId == sample.Attribute.Id).Item2?.ToString(), generatedOptionPropertyData.Where(x => x.Symbol.Equals(sample.Symbol, SymbolEqualityComparer.Default)).Select(x => x.Item2) ) ); - var docFrontMatter = GatherDocumentFrontMatter(ctx, markdownFileData); + var docFrontMatter = GatherDocumentFrontMatter(ctx, markdownProjPairings); if (isExecutingInSampleProject && !skipDiagnostics) { - ReportSampleDiagnostics(ctx, toolkitSampleAttributeData, optionsPaneAttribute, generatedOptionPropertyData); + ReportSampleDiagnostics(ctx, toolkitSampleAttributeData, optionsPaneAttributeData, generatedOptionPropertyData); ReportDocumentDiagnostics(ctx, sampleMetadata, markdownFileData, toolkitSampleAttributeData, docFrontMatter); } From adc35a94549b2087aa18a499e38c2325d6b84008 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 29 Aug 2023 23:36:54 -0700 Subject: [PATCH 4/5] Update Sample App Documentation page source and package links Source code button navigates to component folder on GitHub (main branch) Most likely namespace is shown (hard-coded logic for now) - https://github.com/unoplatform/uno/issues/8750 Shows and links to Uwp/WinUI packages on NuGet --- .../ToolkitDocumentationRenderer.xaml | 10 ++++++---- .../ToolkitDocumentationRenderer.xaml.cs | 19 +++++++++++++++++++ ProjectHeads/App.Head.props | 2 ++ ToolkitComponent.SampleProject.props | 1 + 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CommunityToolkit.App.Shared/Renderers/ToolkitDocumentationRenderer.xaml b/CommunityToolkit.App.Shared/Renderers/ToolkitDocumentationRenderer.xaml index 60dcbdbf..7a9b030d 100644 --- a/CommunityToolkit.App.Shared/Renderers/ToolkitDocumentationRenderer.xaml +++ b/CommunityToolkit.App.Shared/Renderers/ToolkitDocumentationRenderer.xaml @@ -230,7 +230,7 @@ </StackPanel> <interactivity:Interaction.Behaviors> <interactions:EventTriggerBehavior EventName="Click"> - <behaviors:NavigateToUriAction NavigateUri="{x:Bind renderer:ToolkitDocumentationRenderer.ToGitHubUri('issues', Metadata.IssueId), Mode=OneWay}" /> + <behaviors:NavigateToUriAction NavigateUri="{x:Bind renderer:ToolkitDocumentationRenderer.ToComponentUri(Metadata.ComponentName), Mode=OneWay}" /> </interactions:EventTriggerBehavior> </interactivity:Interaction.Behaviors> </Button> @@ -251,21 +251,23 @@ Text="Namespace" /> <TextBlock FontFamily="Consolas" IsTextSelectionEnabled="True" - Text="CommunityToolkit.WinUI.Behaviors" /> + Text="{x:Bind renderer:ToolkitDocumentationRenderer.ToPackageNamespace(Metadata.CsProjName), Mode=OneWay}" /> <TextBlock Margin="0,24,0,0" Foreground="{StaticResource TextFillColorSecondaryBrush}" Style="{StaticResource CaptionTextBlockStyle}" Text="NuGet package" /> <TextBlock IsTextSelectionEnabled="True"> <Hyperlink FontFamily="Consolas" + NavigateUri="{x:Bind renderer:ToolkitDocumentationRenderer.ToPackageUri('Uwp', Metadata.CsProjName), Mode=OneWay}" TextDecorations="None"> - CommunityToolkit.Uwp.Behaviors + <Run Text="{x:Bind renderer:ToolkitDocumentationRenderer.ToPackageName('Uwp', Metadata.CsProjName), Mode=OneWay}" /> </Hyperlink> </TextBlock> <TextBlock IsTextSelectionEnabled="True"> <Hyperlink FontFamily="Consolas" + NavigateUri="{x:Bind renderer:ToolkitDocumentationRenderer.ToPackageUri('WinUI', Metadata.CsProjName), Mode=OneWay}" TextDecorations="None"> - CommunityToolkit.WinUI.Behaviors + <Run Text="{x:Bind renderer:ToolkitDocumentationRenderer.ToPackageName('WinUI', Metadata.CsProjName), Mode=OneWay}" /> </Hyperlink> </TextBlock> </StackPanel> diff --git a/CommunityToolkit.App.Shared/Renderers/ToolkitDocumentationRenderer.xaml.cs b/CommunityToolkit.App.Shared/Renderers/ToolkitDocumentationRenderer.xaml.cs index 9913d4f3..e665a3a5 100644 --- a/CommunityToolkit.App.Shared/Renderers/ToolkitDocumentationRenderer.xaml.cs +++ b/CommunityToolkit.App.Shared/Renderers/ToolkitDocumentationRenderer.xaml.cs @@ -6,6 +6,7 @@ using CommunityToolkit.Tooling.SampleGen.Metadata; using Windows.Storage; using Windows.System; +using static System.Net.WebRequestMethods; #if !HAS_UNO #if !WINAPPSDK @@ -215,6 +216,24 @@ private async void MarkdownTextBlock_LinkClicked(object sender, LinkClickedEvent public static Uri? ToGitHubUri(string path, int id) => IsProjectPathValid() ? new Uri($"{ProjectUrl}/{path}/{id}") : null; + public static Uri? ToComponentUri(string name) => IsProjectPathValid() ? new Uri($"{ProjectUrl}/tree/main/components/{name}") : null; + + public static Uri? ToPackageUri(string platform, string projectFileName) => new Uri($"https://www.nuget.org/packages/{RemoveFileExtension(projectFileName).Replace("WinUI", platform)}"); + + public static string ToPackageName(string platform, string projectFileName) => RemoveFileExtension(projectFileName).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, diff --git a/ProjectHeads/App.Head.props b/ProjectHeads/App.Head.props index 1c6a1177..5e36c67c 100644 --- a/ProjectHeads/App.Head.props +++ b/ProjectHeads/App.Head.props @@ -94,6 +94,7 @@ <!-- Include markdown files from all samples so the head can access them in the source generator --> <AdditionalFiles Include="$(RepositoryDirectory)components\**\samples\**\*.md" Exclude="$(RepositoryDirectory)**\**\samples\**\obj\**\*.md;$(RepositoryDirectory)**\**\samples\**\bin\**\*.md"/> + <AdditionalFiles Include="$(RepositoryDirectory)components\**\src\**\*.csproj" /> </ItemGroup> <!-- See https://github.com/CommunityToolkit/Labs-Windows/issues/142 --> @@ -109,6 +110,7 @@ <!-- Include markdown files from all samples so the head can access them in the source generator --> <AdditionalFiles Include="$(MSBuildProjectDirectory)\..\..\samples\*.md" Exclude="$(MSBuildProjectDirectory)\..\..\**\obj\**\*.md;$(MSBuildProjectDirectory)\..\..\**\bin\**\*.md"/> + <AdditionalFiles Include="$(MSBuildProjectDirectory)\..\..\src\**\*.csproj" /> </ItemGroup> <PropertyGroup> diff --git a/ToolkitComponent.SampleProject.props b/ToolkitComponent.SampleProject.props index 198f526f..da30b80a 100644 --- a/ToolkitComponent.SampleProject.props +++ b/ToolkitComponent.SampleProject.props @@ -38,5 +38,6 @@ <!-- Enable reading Markdown files from source generator --> <AdditionalFiles Include="**\*.md" Exclude="bin\**\*.md;obj\**\*.md" /> + <AdditionalFiles Include="$(MSBuildProjectDirectory)\..\src\*.csproj" /> </ItemGroup> </Project> From 7612a2fd2242fad5ae8679211abad46d31645146 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Wed, 30 Aug 2023 08:02:40 -0700 Subject: [PATCH 5/5] Update IconPath based on changes to generator from Arlo's PR --- .../ToolkitSampleMetadataTests.Documentation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs b/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs index 9b52abb8..9eb02a78 100644 --- a/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs +++ b/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs @@ -263,7 +263,7 @@ public void DocumentationValidWithRegistry() subcategory: Layout discussion-id: 0 issue-id: 0 -icon: assets/icon.png +icon: assets\icon.png experimental: true --- # This is some test documentation... @@ -299,7 +299,7 @@ public static class ToolkitDocumentRegistry { public static System.Collections.Generic.IEnumerable<CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitFrontMatter> Execute() { - yield return new CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitFrontMatter() { ComponentName = "Primitives", Title = "Canvas Layout", Author = "mhawker", Description = "A canvas-like VirtualizingLayout for use in an ItemsRepeater", Keywords = "CanvasLayout, ItemsRepeater, VirtualizingLayout, Canvas, Layout, Panel, Arrange", Category = ToolkitSampleCategory.Controls, Subcategory = ToolkitSampleSubcategory.Layout, DiscussionId = 0, IssueId = 0, Icon = @"experiment/samples/assets/icon.png", FilePath = @"experiment\samples\documentation.md", SampleIdReferences = new string[] { "Sample" }, IsExperimental = true, CsProjName = @"componentname.csproj" }; + yield return new CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitFrontMatter() { ComponentName = "Primitives", Title = "Canvas Layout", Author = "mhawker", Description = "A canvas-like VirtualizingLayout for use in an ItemsRepeater", Keywords = "CanvasLayout, ItemsRepeater, VirtualizingLayout, Canvas, Layout, Panel, Arrange", Category = ToolkitSampleCategory.Controls, Subcategory = ToolkitSampleSubcategory.Layout, DiscussionId = 0, IssueId = 0, Icon = @"assets/icon.png", FilePath = @"experiment\samples\documentation.md", SampleIdReferences = new string[] { "Sample" }, IsExperimental = true, CsProjName = @"componentname.csproj" }; } } """, "Unexpected code generated");