diff --git a/ThisAssembly.sln b/ThisAssembly.sln index 7d966b3c..58695a4b 100644 --- a/ThisAssembly.sln +++ b/ThisAssembly.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30509.190 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33103.184 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThisAssembly.Metadata", "src\ThisAssembly.Metadata\ThisAssembly.Metadata.csproj", "{B5007099-8BE7-490B-9E9A-18CC50C92C29}" EndProject @@ -35,6 +35,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThisAssembly.Constants", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThisAssembly.Tests", "src\ThisAssembly.Tests\ThisAssembly.Tests.csproj", "{AD25424F-7DE0-4515-AE9F-B95414218292}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThisAssembly.Resources", "src\ThisAssembly.Resources\ThisAssembly.Resources.csproj", "{14D0C5BA-8410-4454-87A2-7BF5993E1EA2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -73,6 +75,10 @@ Global {AD25424F-7DE0-4515-AE9F-B95414218292}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD25424F-7DE0-4515-AE9F-B95414218292}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD25424F-7DE0-4515-AE9F-B95414218292}.Release|Any CPU.Build.0 = Release|Any CPU + {14D0C5BA-8410-4454-87A2-7BF5993E1EA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14D0C5BA-8410-4454-87A2-7BF5993E1EA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14D0C5BA-8410-4454-87A2-7BF5993E1EA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14D0C5BA-8410-4454-87A2-7BF5993E1EA2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/readme.md b/readme.md index 911ddaa3..61378bf7 100644 --- a/readme.md +++ b/readme.md @@ -114,6 +114,23 @@ them as `ProjectProperty` MSBuild items in project file, such as: ![](img/ThisAssembly.Project.png) +## ThisAssembly.Resources + +[![Version](https://img.shields.io/nuget/vpre/ThisAssembly.Resources.svg?color=royalblue)](https://www.nuget.org/packages/ThisAssembly.Resources) +[![Downloads](https://img.shields.io/nuget/dt/ThisAssembly.Resources.svg?color=green)](https://www.nuget.org/packages/ThisAssembly.Resources) + +This package generates a static `ThisAssembly.Resources` class with public +properties exposing shortcuts to retrieve the contents of embedded resources. + + +```xml + + + +``` + +![](img/ThisAssembly.Resources.png) + ## ThisAssembly.Strings [![Version](https://img.shields.io/nuget/vpre/ThisAssembly.Strings.svg?color=royalblue)](https://www.nuget.org/packages/ThisAssembly.Strings) diff --git a/src/EmbeddedResource.cs b/src/EmbeddedResource.cs index a5075a92..c738ef09 100644 --- a/src/EmbeddedResource.cs +++ b/src/EmbeddedResource.cs @@ -5,13 +5,28 @@ static class EmbeddedResource { - static readonly string baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + static readonly string baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; public static string GetContent(string relativePath) + { + using var stream = GetStream(relativePath); + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + public static byte[] GetBytes(string relativePath) + { + using var stream = GetStream(relativePath); + var bytes = new byte[stream.Length]; + stream.Read(bytes, 0, bytes.Length); + return bytes; + } + + public static Stream GetStream(string relativePath) { var filePath = Path.Combine(baseDir, Path.GetFileName(relativePath)); if (File.Exists(filePath)) - return File.ReadAllText(filePath); + return File.OpenRead(filePath); var baseName = Assembly.GetExecutingAssembly().GetName().Name; var resourceName = relativePath @@ -25,13 +40,12 @@ public static string GetContent(string relativePath) if (string.IsNullOrEmpty(manifestResourceName)) throw new InvalidOperationException($"Did not find required resource ending in '{resourceName}' in assembly '{baseName}'."); - using var stream = Assembly.GetExecutingAssembly() + var stream = Assembly.GetExecutingAssembly() .GetManifestResourceStream(manifestResourceName); if (stream == null) throw new InvalidOperationException($"Did not find required resource '{manifestResourceName}' in assembly '{baseName}'."); - using var reader = new StreamReader(stream); - return reader.ReadToEnd(); + return stream; } } \ No newline at end of file diff --git a/src/ThisAssembly.Resources/CSharp.sbntxt b/src/ThisAssembly.Resources/CSharp.sbntxt new file mode 100644 index 00000000..407c1c4e --- /dev/null +++ b/src/ThisAssembly.Resources/CSharp.sbntxt @@ -0,0 +1,50 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// ThisAssembly.Resources: {{ Version }} +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +{{ func resource }} + /// + {{~ if $0.Comment ~}} + /// {{ $0.Comment }} + {{~ else ~}} + /// => @"{{ $0.Path }}" + {{~ end ~}} + /// + public static partial class {{ $0.Name }} + { + {{~ if $0.IsText ~}} + private static string text; + public static string Text => + text ??= EmbeddedResource.GetContent(@"{{ $0.Path }}"); + {{~ end ~}} + + public static byte[] GetBytes() => + EmbeddedResource.GetBytes(@"{{ $0.Path }}"); + public static Stream GetStream() => + EmbeddedResource.GetStream(@"{{ $0.Path }}"); + } +{{ end }} +{{ func render }} + public static partial class {{ $0.Name }} + { + {{~ if $0.Resource ~}} + {{- resource $0.Resource ~}} + {{~ else ~}} + {{ render $0.NestedArea }} + {{~ end ~}} + } +{{ end }} + +using System; +using System.IO; + +partial class ThisAssembly +{ + {{ render RootArea }} +} diff --git a/src/ThisAssembly.Resources/Model.cs b/src/ThisAssembly.Resources/Model.cs new file mode 100644 index 00000000..245efccb --- /dev/null +++ b/src/ThisAssembly.Resources/Model.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; + +[DebuggerDisplay("Values = {RootArea.Values.Count}")] +record Model(Area RootArea) +{ + public string Version => Assembly.GetExecutingAssembly().GetName().Version.ToString(3); +} + +[DebuggerDisplay("Name = {Name}")] +record Area(string Name) +{ + public Area? NestedArea { get; private set; } + public Resource? Resource { get; private set; } + + public static Area Load(Resource resource, string rootArea = "Resources") + { + var root = new Area(rootArea); + + // Splits: ([area].)*[name] + var area = root; + var parts = resource.Name.Split(new[] { "\\", "/" }, StringSplitOptions.RemoveEmptyEntries); + foreach (var part in parts.AsSpan()[..^1]) + { + area.NestedArea = new Area(part); + area = area.NestedArea; + } + + area.Resource = resource with { Name = Path.GetFileNameWithoutExtension(parts[^1]), Path = resource.Name, }; + return root; + } +} + +[DebuggerDisplay("{Name}")] +record Resource(string Name, string? Comment, bool IsText) +{ + public string? Path { get; set; } +}; \ No newline at end of file diff --git a/src/ThisAssembly.Resources/Properties/launchSettings.json b/src/ThisAssembly.Resources/Properties/launchSettings.json new file mode 100644 index 00000000..fb947eaa --- /dev/null +++ b/src/ThisAssembly.Resources/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "ThisAssembly.Resources": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\ThisAssembly.Tests\\ThisAssembly.Tests.csproj" + } + } +} diff --git a/src/ThisAssembly.Resources/ResourcesGenerator.cs b/src/ThisAssembly.Resources/ResourcesGenerator.cs new file mode 100644 index 00000000..ea1e3d60 --- /dev/null +++ b/src/ThisAssembly.Resources/ResourcesGenerator.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Scriban; + +namespace ThisAssembly +{ + [Generator(LanguageNames.CSharp)] + public class ResourcesGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput( + spc => spc.AddSource( + "ThisAssembly.Resources.EmbeddedResource.cs", + SourceText.From(EmbeddedResource.GetContent("EmbeddedResource.cs"), Encoding.UTF8))); + + var files = context.AdditionalTextsProvider + .Combine(context.AnalyzerConfigOptionsProvider) + .Where(x => + x.Right.GetOptions(x.Left).TryGetValue("build_metadata.AdditionalFiles.SourceItemType", out var itemType) + && itemType == "EmbeddedResource") + .Where(x => x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Value", out var value) && value != null) + .Select((x, ct) => + { + x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Value", out var resourceName); + x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Kind", out var kind); + x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Comment", out var comment); + return (resourceName!, kind, comment: string.IsNullOrWhiteSpace(comment) ? null : comment); + }) + .Combine(context.AnalyzerConfigOptionsProvider + .Select((p, _) => + { + p.GlobalOptions.TryGetValue("build_property.EmbeddedResourceStringExtensions", out var extensions); + return extensions!; + })); + + context.RegisterSourceOutput( + files, + GenerateSource); + } + + static void GenerateSource(SourceProductionContext spc, ((string resourceName, string? kind, string? comment), string extensions) arg2) + { + var ((resourceName, kind, comment), extensions) = arg2; + + var file = "CSharp.sbntxt"; + var template = Template.Parse(EmbeddedResource.GetContent(file), file); + + var isText = kind != null && kind.Equals("text", StringComparison.OrdinalIgnoreCase) + || extensions.Split(';').Contains(Path.GetFileName(resourceName)); + var root = Area.Load(new Resource(resourceName, comment, isText)); + var model = new Model(root); + + var output = template.Render(model, member => member.Name); + + // Apply formatting since indenting isn't that nice in Scriban when rendering nested + // structures via functions. + output = Microsoft.CodeAnalysis.CSharp.SyntaxFactory.ParseCompilationUnit(output) + .NormalizeWhitespace() + .GetText() + .ToString(); + + spc.AddSource( + $"{resourceName.Replace('\\', '.').Replace('/', '.')}.g.cs", + SourceText.From(output, Encoding.UTF8)); + } + } +} diff --git a/src/ThisAssembly.Resources/ThisAssembly.Resources.csproj b/src/ThisAssembly.Resources/ThisAssembly.Resources.csproj new file mode 100644 index 00000000..a41f1dae --- /dev/null +++ b/src/ThisAssembly.Resources/ThisAssembly.Resources.csproj @@ -0,0 +1,43 @@ + + + + netstandard2.0 + latest + true + enable + + + + ThisAssembly.Resources + + ** C# 9.0+ ONLY ** + This package generates a static `ThisAssembly.Resources` class with public + properties exposing `string` and `Stream` shortcuts to access embedded resources. + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ThisAssembly.Resources/ThisAssembly.Resources.props b/src/ThisAssembly.Resources/ThisAssembly.Resources.props new file mode 100644 index 00000000..d4a06db1 --- /dev/null +++ b/src/ThisAssembly.Resources/ThisAssembly.Resources.props @@ -0,0 +1,7 @@ + + + + .txt;.cs;.sql;.json;.md; + + + diff --git a/src/ThisAssembly.Resources/ThisAssembly.Resources.targets b/src/ThisAssembly.Resources/ThisAssembly.Resources.targets new file mode 100644 index 00000000..5823e988 --- /dev/null +++ b/src/ThisAssembly.Resources/ThisAssembly.Resources.targets @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + %(RelativeDir)%(Filename) + $([System.IO.Path]::GetDirectoryName('%(Link)'))$([System.IO.Path]::DirectorySeparatorChar)$([System.IO.Path]::GetFileNameWithoutExtension('%(Link)')) + %(Extension) + $([System.IO.Path]::GetExtension('%(Link)')) + + + $([MSBuild]::ValueOrDefault('%(AreaPath)', '').Replace('\', '.').Replace('/', '.')) + %(AreaPath)%(FileExtension) + + + + + + diff --git a/src/ThisAssembly.Strings/StringsGenerator.cs b/src/ThisAssembly.Strings/StringsGenerator.cs index 8aafdd70..76e39de6 100644 --- a/src/ThisAssembly.Strings/StringsGenerator.cs +++ b/src/ThisAssembly.Strings/StringsGenerator.cs @@ -34,7 +34,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Combine(context.AnalyzerConfigOptionsProvider) .Where(x => x.Right.GetOptions(x.Left).TryGetValue("build_metadata.AdditionalFiles.SourceItemType", out var itemType) - && itemType == "EmbeddedResource") + && itemType == "ResourceString") .Where(x => x.Right.GetOptions(x.Left).TryGetValue("build_metadata.AdditionalFiles.ManifestResourceName", out var value) && value != null) .Select((x, ct) => { diff --git a/src/ThisAssembly.Strings/ThisAssembly.Strings.targets b/src/ThisAssembly.Strings/ThisAssembly.Strings.targets index 0676ae7e..914f8195 100644 --- a/src/ThisAssembly.Strings/ThisAssembly.Strings.targets +++ b/src/ThisAssembly.Strings/ThisAssembly.Strings.targets @@ -6,7 +6,7 @@ - + diff --git a/src/ThisAssembly.Tests/Tests.cs b/src/ThisAssembly.Tests/Tests.cs index 2c260287..073f0a2b 100644 --- a/src/ThisAssembly.Tests/Tests.cs +++ b/src/ThisAssembly.Tests/Tests.cs @@ -44,5 +44,14 @@ public void CanUseStringsIndexedArguments() [Fact] public void CanUseStringResource() => Assert.Equal("Value", ThisAssembly.Strings.Foo.Bar.Baz); + + [Fact] + public void CanUseTextResource() + => Assert.NotNull(ThisAssembly.Resources.Content.Styles.Custom.Text); + + [Fact] + public void CanUseByteResource() + => Assert.NotNull(ThisAssembly.Resources.Content.Styles.Main.GetBytes()); + } } diff --git a/src/ThisAssembly.Tests/ThisAssembly.Tests.csproj b/src/ThisAssembly.Tests/ThisAssembly.Tests.csproj index ab9c3be6..f453ff01 100644 --- a/src/ThisAssembly.Tests/ThisAssembly.Tests.csproj +++ b/src/ThisAssembly.Tests/ThisAssembly.Tests.csproj @@ -23,6 +23,7 @@ + @@ -39,6 +40,10 @@ PreserveNewest + + + + diff --git a/src/ThisAssembly/ThisAssembly.csproj b/src/ThisAssembly/ThisAssembly.csproj index e6148c51..72c95a53 100644 --- a/src/ThisAssembly/ThisAssembly.csproj +++ b/src/ThisAssembly/ThisAssembly.csproj @@ -28,6 +28,7 @@ +