diff --git a/src/ReswPlus.Shared/ResourceParser/FormatTag.cs b/src/ReswPlus.Shared/ResourceParser/FormatTag.cs index d19f479..b5d479f 100644 --- a/src/ReswPlus.Shared/ResourceParser/FormatTag.cs +++ b/src/ReswPlus.Shared/ResourceParser/FormatTag.cs @@ -56,7 +56,7 @@ public FunctionFormatTagParameter(ParameterType type, string name, ParameterType public sealed class FunctionFormatTagParametersInfo { - public List Parameters { get; set; } = new(); + public List Parameters { get; set; } = []; public FunctionFormatTagParameter? PluralizationParameter { get; set; } public FunctionFormatTagParameter? VariantParameter { get; set; } } diff --git a/src/ReswPlus.Shared/ResourceParser/ReswInfo.cs b/src/ReswPlus.Shared/ResourceParser/ReswInfo.cs index 77cd626..70e29ba 100644 --- a/src/ReswPlus.Shared/ResourceParser/ReswInfo.cs +++ b/src/ReswPlus.Shared/ResourceParser/ReswInfo.cs @@ -8,6 +8,6 @@ public sealed class ReswInfo public ReswInfo() { - Items = new(); + Items = []; } } diff --git a/src/ReswPlus.SourceGenerator/ClassGenerators/Models/Localization.cs b/src/ReswPlus.SourceGenerator/ClassGenerators/Models/Localization.cs index 394c0b0..1bdf527 100644 --- a/src/ReswPlus.SourceGenerator/ClassGenerators/Models/Localization.cs +++ b/src/ReswPlus.SourceGenerator/ClassGenerators/Models/Localization.cs @@ -23,12 +23,12 @@ protected Localization(string key, string summary) /// /// Gets or sets the list of format tag parameters. /// - public List Parameters { get; set; } = new(); + public List Parameters { get; set; } = []; /// /// Gets the list of extra function format tag parameters. /// - public List ExtraParameters { get; } = new(); + public List ExtraParameters { get; } = []; /// /// Gets or sets the summary for the localization. diff --git a/src/ReswPlus.SourceGenerator/ClassGenerators/Models/StronglyTypedClass.cs b/src/ReswPlus.SourceGenerator/ClassGenerators/Models/StronglyTypedClass.cs index a3bd816..503cd32 100644 --- a/src/ReswPlus.SourceGenerator/ClassGenerators/Models/StronglyTypedClass.cs +++ b/src/ReswPlus.SourceGenerator/ClassGenerators/Models/StronglyTypedClass.cs @@ -11,7 +11,7 @@ public StronglyTypedClass(bool isAdvanced, string[] namespaces, string resoureFi ResoureFile = resoureFile; ClassName = className; AppType = appType; - Items = new(); + Items = []; } public bool IsAdvanced { get; } diff --git a/src/ReswPlus.SourceGenerator/ClassGenerators/PluralFormsRetriever.cs b/src/ReswPlus.SourceGenerator/ClassGenerators/PluralFormsRetriever.cs index 8ef8126..d5144bf 100644 --- a/src/ReswPlus.SourceGenerator/ClassGenerators/PluralFormsRetriever.cs +++ b/src/ReswPlus.SourceGenerator/ClassGenerators/PluralFormsRetriever.cs @@ -22,12 +22,11 @@ public PluralForm(string id, string[] languages) /// /// A static collection of predefined plural forms and their associated languages. /// - private static readonly PluralForm[] PluralForms = new PluralForm[] - { + private static readonly PluralForm[] PluralForms = + [ new PluralForm( "IntOneOrZero", - new[] - { + [ "ak", // Akan "bh", // Bihari "guw", // Gun @@ -37,12 +36,11 @@ public PluralForm(string id, string[] languages) "pa", // Punjabi "ti", // Tigrinya "wa" // Walloon - } + ] ), new PluralForm( "ZeroToOne", - new[] - { + [ "am", // Amharic "bn", // Bengali "ff", // Fulah @@ -52,21 +50,19 @@ public PluralForm(string id, string[] languages) "mr", // Marathi "fa", // Persian "zu" // Zulu - } + ] ), new PluralForm( "ZeroToTwoExcluded", - new[] - { + [ "hy", // Armenian "fr", // French "kab" // Kabyle - } + ] ), new PluralForm( "OnlyOne", - new[] - { + [ "af", // Afrikaans "sq", // Albanian "ast", // Asturian @@ -166,138 +162,120 @@ public PluralForm(string id, string[] languages) "xh", // Xhosa "yi", // Yiddish "ji" // Jiddish - } + ] ), new PluralForm( "Sinhala", - new[] - { + [ "si" // Sinhala - } + ] ), new PluralForm( "Latvian", - new[] - { + [ "lv", // Latvian "prg" // Prussian - } + ] ), new PluralForm( "Irish", - new[] - { + [ "ga" // Irish - } + ] ), new PluralForm( "Romanian", - new[] - { + [ "ro", // Romanian "mo" // Moldavian - } + ] ), new PluralForm( "Lithuanian", - new[] - { + [ "lt" // Lithuanian - } + ] ), new PluralForm( "Slavic", - new[] - { + [ "ru", // Russian "uk", // Ukrainian "be" // Belarusian - } + ] ), new PluralForm( "Czech", - new[] - { + [ "cs", // Czech "sk" // Slovak - } + ] ), new PluralForm( "Polish", - new[] - { + [ "pl" // Polish - } + ] ), new PluralForm( "Slovenian", - new[] - { + [ "sl" // Slovenian - } + ] ), new PluralForm( "Arabic", - new[] - { + [ "ar" // Arabic - } + ] ), new PluralForm( "Hebrew", - new[] - { + [ "he", // Hebrew "iw" // (old code for Hebrew) - } + ] ), new PluralForm( "Filipino", - new[] - { + [ "fil", // Filipino "tl" // Tagalog - } + ] ), new PluralForm( "Macedonian", - new[] - { + [ "mk" // Macedonian - } + ] ), new PluralForm( "Breizh", - new[] - { + [ "br" // Breton - } + ] ), new PluralForm( "CentralAtlasTamazight", - new[] - { + [ "tzm" // Central Atlas Tamazight - } + ] ), new PluralForm( "OneOrZero", - new[] - { + [ "ksh" // Colognian - } + ] ), new PluralForm( "OneOrZeroToOneExcluded", - new[] - { + [ "lag" // Langi - } + ] ), new PluralForm( "OneOrTwo", - new[] - { + [ "kw", // Cornish "smn", // Inari Sami "iu", // Inuktitut @@ -307,68 +285,60 @@ public PluralForm(string id, string[] languages) "smi", // Other Sami languages "sms", // Skolt Sami "sma" // Southern Sami - } + ] ), new PluralForm( "Croat", - new[] - { + [ "bs", // Bosnian "hr", // Croatian "sr", // Serbian "sh" // Serbo-Croatian - } + ] ), new PluralForm( "Tachelhit", - new[] - { + [ "shi" // Tachelhit - } + ] ), new PluralForm( "Icelandic", - new[] - { + [ "is" // Icelandic - } + ] ), new PluralForm( "Manx", - new[] - { + [ "gv" // Manx - } + ] ), new PluralForm( "ScottishGaelic", - new[] - { + [ "gd" // Scottish Gaelic - } + ] ), new PluralForm( "Maltese", - new[] - { + [ "mt" // Maltese - } + ] ), new PluralForm( "Welsh", - new[] - { + [ "cy" // Welsh - } + ] ), new PluralForm( "Danish", - new[] - { + [ "da" // Danish - } + ] ) - }; + ]; // Prebuild a dictionary that maps each language code to its plural form. private static readonly Dictionary LanguageToPluralForm = BuildLanguageToPluralForm(); diff --git a/src/ReswPlus.SourceGenerator/ClassGenerators/ReswClassGenerator.cs b/src/ReswPlus.SourceGenerator/ClassGenerators/ReswClassGenerator.cs index 41b0de2..f526ee6 100644 --- a/src/ReswPlus.SourceGenerator/ClassGenerators/ReswClassGenerator.cs +++ b/src/ReswPlus.SourceGenerator/ClassGenerators/ReswClassGenerator.cs @@ -120,7 +120,7 @@ private StronglyTypedClass Parse(string content, string defaultNamespace, bool i // Use the first comment that contains a valid format tag var commentToUse = item.Items.FirstOrDefault(i => !string.IsNullOrEmpty(i.Comment) && _regexStringFormat.IsMatch(i.Comment))?.Comment; - ManageFormattedFunction(localization, commentToUse, basicItems, resourceFileName); + _ = ManageFormattedFunction(localization, commentToUse, basicItems, resourceFileName); result.Items.Add(localization); } else if (item.SupportVariants) @@ -130,7 +130,7 @@ private StronglyTypedClass Parse(string content, string defaultNamespace, bool i var commentToUse = item.Items.FirstOrDefault(i => !string.IsNullOrEmpty(i.Comment) && _regexStringFormat.IsMatch(i.Comment))?.Comment; var localization = new VariantLocalization(itemKey, summary); - ManageFormattedFunction(localization, commentToUse, basicItems, resourceFileName); + _ = ManageFormattedFunction(localization, commentToUse, basicItems, resourceFileName); result.Items.Add(localization); } } @@ -149,7 +149,7 @@ private StronglyTypedClass Parse(string content, string defaultNamespace, bool i if (isAdvanced) { - ManageFormattedFunction(localization, item.Comment, stringItems, resourceFileName); + _ = ManageFormattedFunction(localization, item.Comment, stringItems, resourceFileName); } result.Items.Add(localization); } @@ -165,17 +165,10 @@ private StronglyTypedClass Parse(string content, string defaultNamespace, bool i /// True if the property name is valid; otherwise, false. private static bool IsValidPropertyName(string propertyName) { - if (string.IsNullOrWhiteSpace(propertyName)) - { - return false; - } - - if (!char.IsLetter(propertyName[0]) && propertyName[0] != '_') - { - return false; - } - - return propertyName.Skip(1).All(c => char.IsLetterOrDigit(c) || c == '_'); + return + !string.IsNullOrWhiteSpace(propertyName) && + (char.IsLetter(propertyName[0]) || propertyName[0] == '_') && + propertyName.Skip(1).All(c => char.IsLetterOrDigit(c) || c == '_'); } /// diff --git a/src/ReswPlus.SourceGenerator/CodeGenerators/CodeStringBuilder.cs b/src/ReswPlus.SourceGenerator/CodeGenerators/CodeStringBuilder.cs deleted file mode 100644 index 88d285c..0000000 --- a/src/ReswPlus.SourceGenerator/CodeGenerators/CodeStringBuilder.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Text; - -namespace ReswPlus.SourceGenerator.CodeGenerators; - -/// -/// A helper class to build code strings with indentation support. -/// -/// The string used for indentation. -internal sealed class CodeStringBuilder(string indentString) -{ - private readonly StringBuilder _stringBuilder = new(); - private readonly string _indentString = indentString; - private uint _level = 0; - - /// - /// Appends a line to the string builder. - /// - /// The line to append. - /// Whether to add indentation spaces. - /// The current instance of . - public CodeStringBuilder AppendLine(string value, bool addSpaces = true) - { - if (addSpaces) - { - AddSpaces(_level); - } - _ = _stringBuilder.AppendLine(value); - return this; - } - - /// - /// Adds indentation spaces based on the current level. - /// - /// The current indentation level. - private void AddSpaces(uint level) - { - for (var i = 0; i < level; ++i) - { - _ = _stringBuilder.Append(_indentString); - } - } - - /// - /// Increases the indentation level. - /// - /// The current instance of . - public CodeStringBuilder AddLevel() - { - ++_level; - return this; - } - - /// - /// Decreases the indentation level. - /// - /// The current instance of . - public CodeStringBuilder RemoveLevel() - { - if (_level > 0) - { - --_level; - } - return this; - } - - /// - /// Appends an empty line to the string builder. - /// - /// The current instance of . - public CodeStringBuilder AppendEmptyLine() - { - _ = _stringBuilder.AppendLine(""); - return this; - } - - /// - /// Gets the built string. - /// - /// The built string. - public string GetString() - { - return _stringBuilder.ToString(); - } -} diff --git a/src/ReswPlus.SourceGenerator/CodeGenerators/CsharpCodeGenerator.cs b/src/ReswPlus.SourceGenerator/CodeGenerators/CsharpCodeGenerator.cs index f8ac048..7e91b9b 100644 --- a/src/ReswPlus.SourceGenerator/CodeGenerators/CsharpCodeGenerator.cs +++ b/src/ReswPlus.SourceGenerator/CodeGenerators/CsharpCodeGenerator.cs @@ -1,368 +1,855 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using ReswPlus.Core.ResourceParser; using ReswPlus.SourceGenerator.ClassGenerators.Models; using ReswPlus.SourceGenerator.Models; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace ReswPlus.SourceGenerator.CodeGenerators; /// -/// Generates C# code for strongly-typed resources based on localization files. +/// Generates C# code for strongly-typed resources using Roslyn SyntaxFactory. /// +/// +/// This class converts resource files and localization keys into a strongly-typed class +/// that exposes methods or properties for each resource key. It also creates a XAML markup +/// extension class to allow for resource binding in XAML. The generated source code is well-formatted, +/// includes generated attributes, and organizes each resource member within region directives. +/// +/// +/// The generated code may look similar to the following: +/// +/// // File generated automatically by ReswPlus. https://github.com/DotNetPlus/ReswPlus +/// using System; +/// using Windows.UI.Xaml.Markup; +/// using Windows.UI.Xaml.Data; +/// +/// namespace MyApp.Resources { +/// [global::System.CodeDom.Compiler.GeneratedCodeAttribute("ReswPlus", "1.0.0.0")] +/// [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +/// [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] +/// public static class MyResources { +/// private static _ReswPlus_AutoGenerated.ResourceStringProvider _resourceStringProvider; +/// +/// static MyResources() { +/// _resourceStringProvider = new _ReswPlus_AutoGenerated.ResourceStringProvider("MyResourceFile.resw"); +/// } +/// +/// public static string GetString(string key) => _resourceStringProvider.GetString(key); +/// +/// #region WelcomeMessage +/// /// +/// /// This is the welcome message. +/// /// +/// public static string WelcomeMessage { +/// get { return _resourceStringProvider.GetString("WelcomeMessage"); } +/// } +/// #endregion +/// +/// // Additional generated members... +/// } +/// } +/// +/// internal sealed class CSharpCodeGenerator : ICodeGenerator { /// - /// Generates the C# files based on the provided strongly-typed class information and resource file information. + /// Generates a collection of files containing the source code. /// - /// The base filename to be used for the generated files. - /// The strongly-typed class information used for generating code. - /// The resource file information used to generate the C# files. + /// The base filename for the generated file. + /// Contains information about the strongly-typed class and its localization items. + /// Contains information about the resource file. /// A collection of generated files. public IEnumerable GetGeneratedFiles(string? baseFilename, StronglyTypedClass info, ResourceFileInfo resourceFileInfo) { - var builder = new CodeStringBuilder(resourceFileInfo.Project.GetIndentString()); - GenerateHeaders(builder, info.IsAdvanced, info.AppType); - AddNewLine(builder); - OpenNamespace(builder, info.Namespaces); - OpenStronglyTypedClass(builder, info.ResoureFile, info.ClassName); - - foreach (var item in info.Items) - { - AddNewLine(builder); - OpenRegion(builder, item.Key); - - CreateFormatMethod( - builder, - item.Key, - item.IsProperty, - item.Parameters, - item.Summary, - item.ExtraParameters, - (item as PluralLocalization)?.ParameterToUseForPluralization, - (item as PluralLocalization)?.SupportNoneState ?? false, - (item as IVariantLocalization)?.ParameterToUseForVariant); - - CloseRegion(builder, item.Key); - } + // Create a header comment that will be placed at the top of the generated file. + var headerTrivia = TriviaList( + Comment("// File generated automatically by ReswPlus. https://github.com/DotNetPlus/ReswPlus"), + CarriageReturnLineFeed + ); - CloseStronglyTypedClass(builder); - AddNewLine(builder); - CreateMarkupExtension(builder, info.ResoureFile, $"{info.ClassName}Extension", info.Items.OfType().Select(s => s.Key)); - CloseNamespace(builder, info.Namespaces); + // Build the compilation unit (the root node of a C# file) and add required using directives. + var compilationUnit = CompilationUnit() + .WithLeadingTrivia(headerTrivia) + .WithUsings(List(GetUsings(info.AppType))); - return GetGeneratedFiles(builder, baseFilename); - } + // Create the strongly-typed static class declaration (the class that will provide resource lookup). + var strongClassDecl = CreateStronglyTypedClass(info); - /// - /// Converts a parameter type to its corresponding C# type string. - /// - /// The parameter type. - /// The corresponding C# type string. - private string GetParameterTypeString(ParameterType type) => type switch - { - ParameterType.Byte => "byte", - ParameterType.Int => "int", - ParameterType.Uint => "uint", - ParameterType.Long => "long", - ParameterType.String => "string", - ParameterType.Double => "double", - ParameterType.Char => "char", - ParameterType.Ulong => "ulong", - ParameterType.Decimal => "decimal", - _ => "object", - }; + // For each localization item, generate members (properties or methods) that return localized strings. + var formatMembers = new List(); + foreach (var item in info.Items) + { + // Generate one or more member declarations for the given localization key. + var membersForItem = CreateFormatMethodSyntax(item).ToList(); - /// - /// Creates the generated file object for the C# code. - /// - /// The code string builder containing the generated C# code. - /// The base filename for the generated file. - /// An enumerable containing the generated file. - private IEnumerable GetGeneratedFiles(CodeStringBuilder builder, string? baseFilename) - { - yield return new GeneratedFile(baseFilename + ".cs", builder.GetString()); - } + // Instead of using simple comment trivia, create structured region directives for better readability. + for (int i = 0; i < membersForItem.Count; i++) + { + // Create a #region directive with the key name. + var regionDirectiveTrivia = SyntaxFactory.Trivia( + SyntaxFactory.RegionDirectiveTrivia(isActive: true) + .WithEndOfDirectiveToken( + SyntaxFactory.Token( + leading: SyntaxFactory.TriviaList(), + kind: SyntaxKind.EndOfDirectiveToken, + text: " " + item.Key, + valueText: " " + item.Key, + trailing: SyntaxFactory.TriviaList(SyntaxFactory.ElasticCarriageReturnLineFeed) + ) + ) + ); + // Create an #endregion directive. + var endRegionDirectiveTrivia = SyntaxFactory.Trivia( + SyntaxFactory.EndRegionDirectiveTrivia(isActive: true) + .WithEndOfDirectiveToken( + SyntaxFactory.Token( + leading: SyntaxFactory.TriviaList(), + kind: SyntaxKind.EndOfDirectiveToken, + text: string.Empty, + valueText: string.Empty, + trailing: SyntaxFactory.TriviaList(SyntaxFactory.ElasticCarriageReturnLineFeed) + ) + ) + ); + // Attach the region directives as leading and trailing trivia. + membersForItem[i] = membersForItem[i] + .WithLeadingTrivia(SyntaxFactory.TriviaList(regionDirectiveTrivia)) + .WithTrailingTrivia(SyntaxFactory.TriviaList(endRegionDirectiveTrivia)); + } + formatMembers.AddRange(membersForItem); + } + // Add the generated members (format methods/properties) to the strongly-typed class. + strongClassDecl = strongClassDecl.AddMembers(formatMembers.ToArray()); - /// - /// Generates the headers for the C# file, including necessary using statements and comments. - /// - /// The code string builder to append the headers to. - /// Indicates whether pluralization support is needed. - /// The type of application (to determine which namespaces to use). - private void GenerateHeaders(CodeStringBuilder builder, bool supportPluralization, AppType appType) - { - builder.AppendLine("// File generated automatically by ReswPlus. https://github.com/DotNetPlus/ReswPlus") - .AppendLine("using System;"); + // Create the markup extension class that allows resource keys to be used in XAML. + var markupExtensionDecl = CreateMarkupExtensionSyntax(info.ResoureFile, info.ClassName + "Extension", info.Items.Select(x => x.Key)); - if (appType is AppType.WindowsAppSDK) + // If a namespace is provided, wrap the classes in a namespace declaration. + if (info.Namespaces != null && info.Namespaces.Any()) { - builder.AppendLine("using Microsoft.UI.Xaml.Markup;") - .AppendLine("using Microsoft.UI.Xaml.Data;"); + var nsName = string.Join(".", info.Namespaces); + var namespaceDecl = NamespaceDeclaration(ParseName(nsName)) + .AddMembers(strongClassDecl, markupExtensionDecl); + compilationUnit = compilationUnit.AddMembers(namespaceDecl); } else { - builder.AppendLine("using Windows.UI.Xaml.Markup;") - .AppendLine("using Windows.UI.Xaml.Data;"); + // Otherwise, add the classes at the root level. + compilationUnit = compilationUnit.AddMembers(strongClassDecl, markupExtensionDecl); } + + // Normalize the whitespace (formatting) and return the generated source code. + var code = compilationUnit.NormalizeWhitespace().ToFullString(); + yield return new GeneratedFile(baseFilename + ".cs", code); } /// - /// Opens the namespace block in the generated C# code. + /// Returns a collection of using directives based on the application type. /// - /// The code string builder to append the namespace block to. - /// The collection of namespaces for the generated class. - private void OpenNamespace(CodeStringBuilder builder, IEnumerable namespaces) + private IEnumerable GetUsings(AppType appType) { - if (namespaces != null && namespaces.Any()) + var usings = new List { - var ns = string.Join(".", namespaces); - builder.AppendLine($"namespace {ns}{{") - .AddLevel(); - } - } + UsingDirective(ParseName("System")) + }; - /// - /// Closes the namespace block in the generated C# code. - /// - /// The code string builder to append the closing namespace block to. - /// The collection of namespaces for the generated class. - private void CloseNamespace(CodeStringBuilder builder, IEnumerable namespaces) - { - if (namespaces != null && namespaces.Any()) + if (appType == AppType.WindowsAppSDK) { - var ns = string.Join(".", namespaces); - builder.RemoveLevel() - .AppendLine($"}} // {ns}"); + usings.Add(UsingDirective(ParseName("Microsoft.UI.Xaml.Markup"))); + usings.Add(UsingDirective(ParseName("Microsoft.UI.Xaml.Data"))); } + else + { + usings.Add(UsingDirective(ParseName("Windows.UI.Xaml.Markup"))); + usings.Add(UsingDirective(ParseName("Windows.UI.Xaml.Data"))); + } + + return usings; } /// - /// Opens the strongly-typed class block in the generated C# code. + /// Creates the strongly-typed resource class. + /// This class is static, contains generated attributes, a private resource provider field, + /// a static constructor to initialize it, and a GetString method to retrieve strings. /// - /// The code string builder to append the class block to. - /// The resource file name associated with the strongly-typed class. - /// The name of the strongly-typed class. - private void OpenStronglyTypedClass(CodeStringBuilder builder, string? resourceFileName, string className) + private ClassDeclarationSyntax CreateStronglyTypedClass(StronglyTypedClass info) { - var assembly = typeof(CSharpCodeGenerator).Assembly; - builder.AppendLine($"[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"{assembly.GetName().Name}\", \"{assembly.GetName().Version}\")]") - .AppendLine("[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]") - .AppendLine("[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]") - .AppendLine($"public static class {className} {{") - .AddLevel() - .AppendLine("private static _ReswPlus_AutoGenerated.ResourceStringProvider _resourceStringProvider;") - .AppendLine($"static {className}()") - .AppendLine("{") - .AddLevel() - .AppendLine($"_resourceStringProvider = new _ReswPlus_AutoGenerated.ResourceStringProvider(\"{resourceFileName}\");") - .RemoveLevel() - .AppendLine("}") - .AppendEmptyLine() - .AppendLine("/// ") - .AppendLine("/// Returns the specified string resource for the specified culture or current UI culture.") - .AppendLine("/// ") - .AppendLine("public static string GetString(string key) => _resourceStringProvider.GetString(key);"); - } + // Define attributes for the generated class. + var assemblyName = "ReswPlus"; // This can be determined dynamically. + var version = "1.0.0.0"; + var attributes = List( + [ + AttributeList( + SingletonSeparatedList( + Attribute(ParseName("global::System.CodeDom.Compiler.GeneratedCodeAttribute")) + .WithArgumentList( + AttributeArgumentList( + SeparatedList(new SyntaxNodeOrToken[] + { + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(assemblyName))), + Token(SyntaxKind.CommaToken), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(version))) + })))) + ), + AttributeList(SingletonSeparatedList(Attribute(ParseName("global::System.Diagnostics.DebuggerNonUserCodeAttribute")))), + AttributeList(SingletonSeparatedList(Attribute(ParseName("global::System.Runtime.CompilerServices.CompilerGeneratedAttribute")))) + ]); - /// - /// Closes the strongly-typed class block in the generated C# code. - /// - /// The code string builder to append the closing class block to. - private void CloseStronglyTypedClass(CodeStringBuilder builder) => - builder.RemoveLevel().AppendLine("}"); + // Create a private static field: _resourceStringProvider. + var resourceField = FieldDeclaration( + VariableDeclaration(ParseTypeName("_ReswPlus_AutoGenerated.ResourceStringProvider")) + .WithVariables(SingletonSeparatedList( + VariableDeclarator(Identifier("_resourceStringProvider")) + ))) + .WithModifiers(TokenList( + Token(SyntaxKind.PrivateKeyword), + Token(SyntaxKind.StaticKeyword) + )); - /// - /// Opens a region block in the generated C# code. - /// - /// The code string builder to append the region block to. - /// The name of the region. - private void OpenRegion(CodeStringBuilder builder, string? name) => - builder.AppendLine($"#region {name}"); + // Create a static constructor that initializes the _resourceStringProvider. + var staticCtor = ConstructorDeclaration(info.ClassName) + .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) + .WithBody(Block( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("_resourceStringProvider"), + ObjectCreationExpression(ParseTypeName("_ReswPlus_AutoGenerated.ResourceStringProvider")) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(info.ResoureFile)) + ) + ) + ) + ) + ) + ) + )); - /// - /// Closes a region block in the generated C# code. - /// - /// The code string builder to append the closing region block to. - /// The name of the region. - private void CloseRegion(CodeStringBuilder builder, string? name) => - builder.AppendLine("#endregion"); + // Create a simple GetString method that returns the string for a given key. + var getStringMethod = MethodDeclaration(PredefinedType(Token(SyntaxKind.StringKeyword)), "GetString") + .WithModifiers(TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword) + )) + .WithParameterList( + ParameterList( + SingletonSeparatedList( + Parameter(Identifier("key")) + .WithType(PredefinedType(Token(SyntaxKind.StringKeyword))) + ) + ) + ) + .WithExpressionBody( + ArrowExpressionClause( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("_resourceStringProvider"), + IdentifierName("GetString") + ) + ).WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument(IdentifierName("key")) + ) + ) + ) + ) + ) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); + + // Build and return the complete class declaration. + var classDecl = ClassDeclaration(info.ClassName) + .WithAttributeLists(attributes) + .WithModifiers(TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword) + )) + .AddMembers(resourceField, staticCtor, getStringMethod); + + return classDecl; + } /// - /// Creates a format method for localization keys, including handling pluralization and variants if applicable. + /// Generates one or more member declarations (property or method overloads) + /// for a given localization item. /// - /// The code string builder to append the format method to. - /// The localization key. - /// Indicates whether the key should be treated as a property. - /// The parameters for the format method. - /// The summary documentation for the method. - /// Extra parameters for the format method. - /// The parameter to be used for pluralization. - /// Indicates whether the "none" state is supported in pluralization. - /// The parameter to be used for variants. - private void CreateFormatMethod( - CodeStringBuilder builder, - string key, - bool isProperty, - IEnumerable parameters, - string? summary = null, - IEnumerable? extraParameters = null, - FunctionFormatTagParameter? parameterForPluralization = null, - bool supportNoneState = false, - FunctionFormatTagParameter? parameterForVariant = null) + private IEnumerable CreateFormatMethodSyntax(Localization item) { - // Documentation header. - builder.AppendLine("/// ") - .AppendLine($"/// {summary}") - .AppendLine("/// "); + var members = new List(); + + // Create XML documentation for the member using the summary from the localization item. + var summaryText = item.Summary ?? string.Empty; + var xmlComment = TriviaList( + Trivia( + DocumentationCommentTrivia(SyntaxKind.SingleLineDocumentationCommentTrivia, + List(new XmlNodeSyntax[] + { + // Leading "/// " text. + XmlText().WithTextTokens( + TokenList(XmlTextLiteral("/// ")) + ), + // The summary element. + XmlElement( + XmlElementStartTag(XmlName("summary")), + XmlElementEndTag(XmlName("summary")) + ).WithContent( + List(new XmlNodeSyntax[] + { + XmlText().WithTextTokens(TokenList(XmlTextLiteral(summaryText))) + }) + ) + }) + ) + ) + ); - if (isProperty) + if (item.IsProperty) { - builder.AppendLine($"public static string {key}") - .AppendLine("{") - .AddLevel() - .AppendLine("get"); + // Generate a property: public static string {Key} { get { return ... } } + var propertyDecl = PropertyDeclaration(PredefinedType(Token(SyntaxKind.StringKeyword)), Identifier(item.Key)) + .WithModifiers(TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword) + )) + .WithLeadingTrivia(xmlComment) + .WithAccessorList( + AccessorList( + SingletonList( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithBody(Block(GenerateFormatMethodBody(item))) + ) + ) + ); + members.Add(propertyDecl); } else { - // Combine parameters: start with extra parameters (if any) and then add regular parameters. - var functionParameters = parameters?.OfType().ToList() ?? new(); - if (extraParameters?.Any() == true) + // Build the parameter list from regular parameters and any extra parameters. + var functionParameters = item.Parameters.OfType().ToList(); + if (item.ExtraParameters != null && item.ExtraParameters.Any()) { - functionParameters.InsertRange(0, extraParameters); + functionParameters.InsertRange(0, item.ExtraParameters); } - // If any parameter is marked as a VariantId, create an overload accepting object. + // If any parameter is marked as a variant, generate an overload that uses 'object'. if (functionParameters.Any(p => p.IsVariantId)) { - var genericParams = string.Join(", ", functionParameters.Select(p => $"{(p.IsVariantId ? "object" : GetParameterTypeString(p.Type))} {p.Name}")); - builder.AppendLine($"public static string {key}({genericParams})") - .AppendLine("{") - .AddLevel() - .AppendLine("try") - .AppendLine("{") - .AddLevel() - .AppendLine("return " + key + "(" + string.Join(", ", functionParameters.Select(p => p.IsVariantId ? $"Convert.ToInt64({p.Name})" : p.Name)) + ");") - .RemoveLevel() - .AppendLine("}") - .AppendLine("catch") - .AppendLine("{") - .AddLevel() - .AppendLine("return string.Empty;") - .RemoveLevel() - .AppendLine("}") - .RemoveLevel() - .AppendLine("}") - .AppendEmptyLine() - .AppendLine("/// ") - .AppendLine($"/// {summary}") - .AppendLine("/// "); - } + // Create parameters for the overload. + var overloadParameters = SeparatedList( + functionParameters.Select(p => + Parameter(Identifier(p.Name)) + .WithType(p.IsVariantId ? PredefinedType(Token(SyntaxKind.ObjectKeyword)) : GetParameterTypeSyntax(p.Type)) + ) + ); - var parametersStr = string.Join(", ", functionParameters.Select(p => $"{GetParameterTypeString(p.Type)} {p.Name}")); - builder.AppendLine($"public static string {key}({parametersStr})"); - } + // Build the inner call arguments, converting variant parameters if needed. + var innerArguments = SeparatedList( + functionParameters.Select(p => + Argument( + p.IsVariantId + ? InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("Convert"), + IdentifierName("ToInt64") + ), + ArgumentList(SingletonSeparatedList(Argument(IdentifierName(p.Name)))) + ) + : IdentifierName(p.Name) + ) + ) + ); - builder.AppendLine("{") - .AddLevel(); + // Create a method overload that wraps the call in a try-catch. + var overloadMethod = MethodDeclaration(PredefinedType(Token(SyntaxKind.StringKeyword)), Identifier(item.Key)) + .WithModifiers(TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword) + )) + .WithParameterList(ParameterList(overloadParameters)) + .WithLeadingTrivia(xmlComment) + .WithBody( + Block( + // try { return {Key}(...); } catch { return string.Empty; } + TryStatement( + Block( + SingletonList( + ReturnStatement( + InvocationExpression(IdentifierName(item.Key)) + .WithArgumentList(ArgumentList(innerArguments)) + ) + ) + ), + List( + [ + CatchClause() + .WithBlock( + Block( + SingletonList( + ReturnStatement( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("string"), + IdentifierName("Empty") + ) + ) + ) + ) + ) + ]), + null + ) + ) + ); + members.Add(overloadMethod); - // Determine the key to use for lookup (for variant keys, append a suffix). - var keyToUse = parameterForVariant != null ? $"\"{key}_Variant\" + {parameterForVariant.Name}" : $"\"{key}\""; + // Also create the standard method with proper parameter types. + var stdParameters = SeparatedList( + functionParameters.Select(p => + Parameter(Identifier(p.Name)) + .WithType(GetParameterTypeSyntax(p.Type)) + ) + ); + var stdMethod = MethodDeclaration(PredefinedType(Token(SyntaxKind.StringKeyword)), Identifier(item.Key)) + .WithModifiers(TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword) + )) + .WithParameterList(ParameterList(stdParameters)) + .WithLeadingTrivia(xmlComment) + .WithBody(Block(GenerateFormatMethodBody(item))); + members.Add(stdMethod); + } + else + { + // If no variant parameter exists, create a single method. + var parametersList = SeparatedList( + functionParameters.Select(p => + Parameter(Identifier(p.Name)) + .WithType(GetParameterTypeSyntax(p.Type)) + ) + ); + var methodDecl = MethodDeclaration(PredefinedType(Token(SyntaxKind.StringKeyword)), Identifier(item.Key)) + .WithModifiers(TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword) + )) + .WithParameterList(ParameterList(parametersList)) + .WithLeadingTrivia(xmlComment) + .WithBody(Block(GenerateFormatMethodBody(item))); + members.Add(methodDecl); + } + } + return members; + } - string localizationStr; - if (parameterForPluralization != null) + /// + /// Generates the statements for the body of a format method. + /// This method builds an expression to look up a localized string, optionally + /// performing formatting via string.Format if formatting parameters exist. + /// + private IEnumerable GenerateFormatMethodBody(Localization item) + { + // Determine the key to use when retrieving the resource. + ExpressionSyntax keyExpr; + if (item is IVariantLocalization variantLoc && variantLoc.ParameterToUseForVariant != null) { - var pluralNumber = parameterForPluralization.TypeToCast.HasValue - ? $"({GetParameterTypeString(parameterForPluralization.TypeToCast.Value)}){parameterForPluralization.Name}" - : parameterForPluralization.Name; - localizationStr = $"_ReswPlus_AutoGenerated.Plurals.ResourceLoaderExtension.GetPlural(_resourceStringProvider, {keyToUse}, {pluralNumber}, {(supportNoneState ? "true" : "false")})"; + // If the item supports variants, concatenate the key with the variant parameter. + keyExpr = BinaryExpression( + SyntaxKind.AddExpression, + LiteralExpression(SyntaxKind.StringLiteralExpression, Literal($"{item.Key}_Variant")), + IdentifierName(variantLoc.ParameterToUseForVariant.Name) + ); } else { - localizationStr = $"_resourceStringProvider.GetString({keyToUse})"; + // Otherwise, just use the key as-is. + keyExpr = LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(item.Key)); } - if (parameters != null && parameters.Any()) + // Build the expression that retrieves the localized string. + ExpressionSyntax localizationExpr; + if (item is PluralLocalization pluralLoc && pluralLoc.ParameterToUseForPluralization != null) { - var formatParameters = string.Join(", ", parameters.Select(p => - p switch - { - FunctionFormatTagParameter functionParam => functionParam.Name, - MacroFormatTagParameter macroParam => $"_ReswPlus_AutoGenerated.Macros.{macroParam.Id}", - LiteralStringFormatTagParameter constStringParameter => $"\"{constStringParameter.Value}\"", - StringRefFormatTagParameter localizationStringParameter => localizationStringParameter.Id, - _ => string.Empty, // fallback; should not happen - })); - builder.AppendLine($"return string.Format({localizationStr}, {formatParameters});"); + // For plural localization, call the GetPlural method. + ExpressionSyntax pluralParamExpr = IdentifierName(pluralLoc.ParameterToUseForPluralization.Name); + if (pluralLoc.ParameterToUseForPluralization.TypeToCast.HasValue) + { + // Cast the parameter if needed. + pluralParamExpr = ParenthesizedExpression( + CastExpression( + GetParameterTypeSyntax(pluralLoc.ParameterToUseForPluralization.TypeToCast.Value), + IdentifierName(pluralLoc.ParameterToUseForPluralization.Name) + ) + ); + } + localizationExpr = InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + ParseExpression("_ReswPlus_AutoGenerated.Plurals.ResourceLoaderExtension"), + IdentifierName("GetPlural") + ), + ArgumentList(SeparatedList( + [ + Argument(IdentifierName("_resourceStringProvider")), + Argument(keyExpr), + Argument(pluralParamExpr), + Argument(LiteralExpression(pluralLoc.SupportNoneState ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression)) + ])) + ); } else { - builder.AppendLine($"return {localizationStr};"); + // For non-plural strings, call GetString. + localizationExpr = InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("_resourceStringProvider"), + IdentifierName("GetString") + ), + ArgumentList(SingletonSeparatedList(Argument(keyExpr))) + ); } - if (isProperty) + // If formatting parameters are provided, wrap the localization string in string.Format. + if (item.Parameters != null && item.Parameters.Any()) { - builder.RemoveLevel() - .AppendLine("}"); + var formatArgs = SeparatedList( + item.Parameters.Select(p => + { + switch (p) + { + case FunctionFormatTagParameter funcParam: + return IdentifierName(funcParam.Name); + case MacroFormatTagParameter macroParam: + return MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + ParseExpression("_ReswPlus_AutoGenerated.Macros"), + IdentifierName(macroParam.Id) + ); + case LiteralStringFormatTagParameter literalParam: + return LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(literalParam.Value)); + case StringRefFormatTagParameter stringRefParam: + return IdentifierName(stringRefParam.Id); + default: + return LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(string.Empty)); + } + }) + ); + + // Call string.Format(localizationExpr, new object[] { ... }); + var formatInvocation = InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("string"), + IdentifierName("Format") + ), + ArgumentList(SeparatedList( + [ + Argument(localizationExpr), + Argument( + ArrayCreationExpression( + ArrayType(PredefinedType(Token(SyntaxKind.ObjectKeyword))) + .WithRankSpecifiers( + SingletonList( + ArrayRankSpecifier( + SingletonSeparatedList( + OmittedArraySizeExpression() + ) + ) + ) + ) + ) + .WithInitializer( + InitializerExpression( + SyntaxKind.ArrayInitializerExpression, + formatArgs + ) + ) + ) + ])) + ); + yield return ReturnStatement(formatInvocation); } + else + { + // No formatting parameters: simply return the localization string. + yield return ReturnStatement(localizationExpr); + } + } + + /// + /// Converts a ParameterType enum value to its corresponding C# type. + /// + private TypeSyntax GetParameterTypeSyntax(ParameterType type) + { + var keyword = type switch + { + ParameterType.Byte => SyntaxKind.ByteKeyword, + ParameterType.Int => SyntaxKind.IntKeyword, + ParameterType.Uint => SyntaxKind.UIntKeyword, + ParameterType.Long => SyntaxKind.LongKeyword, + ParameterType.String => SyntaxKind.StringKeyword, + ParameterType.Double => SyntaxKind.DoubleKeyword, + ParameterType.Char => SyntaxKind.CharKeyword, + ParameterType.Ulong => SyntaxKind.ULongKeyword, + ParameterType.Decimal => SyntaxKind.DecimalKeyword, + _ => SyntaxKind.ObjectKeyword, + }; - builder.RemoveLevel() - .AppendLine("}"); + return PredefinedType(Token(keyword)); } /// - /// Creates a markup extension class for the strongly-typed resource class. + /// Creates a markup extension class that can be used in XAML for resource lookup. + /// This class includes an embedded KeyEnum, a static resource provider field, + /// a static constructor, properties for Key, Converter, ConverterParameter, and + /// a ProvideValue method override. /// - /// The code string builder to append the markup extension class to. - /// The name of the resource file associated with the class. - /// The name of the markup extension class. - /// The collection of keys for the strongly-typed resource class. - private void CreateMarkupExtension(CodeStringBuilder builder, string? resourceFileName, string? className, IEnumerable keys) + private ClassDeclarationSyntax CreateMarkupExtensionSyntax(string resourceFileName, string className, IEnumerable keys) { - var assembly = typeof(CSharpCodeGenerator).Assembly; - builder.AppendLine($"[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"{assembly.GetName().Name}\", \"{assembly.GetName().Version}\")]") - .AppendLine("[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]") - .AppendLine("[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]") - .AppendLine("[MarkupExtensionReturnType(ReturnType = typeof(string))]") - .AppendLine($"public partial class {className} : MarkupExtension") - .AppendLine("{") - .AddLevel() - .AppendLine("public enum KeyEnum") - .AppendLine("{") - .AddLevel() - .AppendLine("_Undefined = 0,"); + var assemblyName = "ReswPlus"; + var version = "1.0.0.0"; + var attributes = List( + [ + AttributeList( + SingletonSeparatedList( + Attribute(ParseName("global::System.CodeDom.Compiler.GeneratedCodeAttribute")) + .WithArgumentList( + AttributeArgumentList( + SeparatedList(new SyntaxNodeOrToken[] + { + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(assemblyName))), + Token(SyntaxKind.CommaToken), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(version))) + }) + ) + ) + ) + ), + AttributeList(SingletonSeparatedList(Attribute(ParseName("global::System.Diagnostics.DebuggerNonUserCodeAttribute")))), + AttributeList(SingletonSeparatedList(Attribute(ParseName("global::System.Runtime.CompilerServices.CompilerGeneratedAttribute")))), + AttributeList( + SingletonSeparatedList( + Attribute(ParseName("MarkupExtensionReturnType")) + .WithArgumentList( + AttributeArgumentList( + SingletonSeparatedList( + AttributeArgument( + NameEquals(IdentifierName("ReturnType")), + null, + TypeOfExpression(PredefinedType(Token(SyntaxKind.StringKeyword))) + ) + ) + ) + ) + ) + ) + ]); + // Build the KeyEnum that lists all available resource keys. + var enumMembers = new List + { + // The default _Undefined member with value 0. + EnumMemberDeclaration(Identifier("_Undefined")) + .WithEqualsValue(EqualsValueClause(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(0)))) + }; foreach (var key in keys) { - builder.AppendLine($"{key},"); + enumMembers.Add(EnumMemberDeclaration(Identifier(key))); } + var keyEnum = EnumDeclaration("KeyEnum") + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) + .WithMembers(SeparatedList(enumMembers)); - builder.RemoveLevel() - .AppendLine("}") - .AppendEmptyLine() - .AppendLine("private static _ReswPlus_AutoGenerated.ResourceStringProvider _resourceStringProvider;") - .AppendLine($"static {className}()") - .AppendLine("{") - .AddLevel() - .AppendLine($"_resourceStringProvider = new _ReswPlus_AutoGenerated.ResourceStringProvider(\"{resourceFileName}\");") - .RemoveLevel() - .AppendLine("}") - .AppendLine("public KeyEnum Key { get; set; }") - .AppendLine("public IValueConverter Converter { get; set; }") - .AppendLine("public object ConverterParameter { get; set; }") - .AppendLine("protected override object ProvideValue()") - .AppendLine("{") - .AddLevel() - .AppendLine("var value = Key == KeyEnum._Undefined ? string.Empty : _resourceStringProvider.GetString(Key.ToString());") - .AppendLine("return Converter is null ? value : Converter.Convert(value, typeof(string), ConverterParameter, null);") - .RemoveLevel() - .AppendLine("}") - .RemoveLevel() - .AppendLine("}"); - } + // Create a private static field for the resource provider. + var resourceField = FieldDeclaration( + VariableDeclaration(ParseTypeName("_ReswPlus_AutoGenerated.ResourceStringProvider")) + .WithVariables(SingletonSeparatedList( + VariableDeclarator(Identifier("_resourceStringProvider")) + )) + ) + .WithModifiers(TokenList( + Token(SyntaxKind.PrivateKeyword), + Token(SyntaxKind.StaticKeyword) + )); - /// - /// Adds an empty line to the generated C# code. - /// - /// The code string builder to append the empty line to. - private void AddNewLine(CodeStringBuilder builder) => builder.AppendEmptyLine(); -} + // Create the static constructor for the markup extension class. + var staticCtor = ConstructorDeclaration(className) + .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) + .WithBody(Block( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("_resourceStringProvider"), + ObjectCreationExpression(ParseTypeName("_ReswPlus_AutoGenerated.ResourceStringProvider")) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(resourceFileName)) + ) + ) + ) + ) + ) + ) + )); + + // Create the ProvideValue method that returns the resource string (possibly using a converter). + var provideValueMethod = MethodDeclaration(PredefinedType(Token(SyntaxKind.ObjectKeyword)), "ProvideValue") + .WithModifiers(TokenList( + Token(SyntaxKind.ProtectedKeyword), + Token(SyntaxKind.OverrideKeyword) + )) + .WithBody(Block( + // Declare a local variable 'value' that checks if the key is _Undefined. + LocalDeclarationStatement( + VariableDeclaration(ParseTypeName("var")) + .WithVariables(SingletonSeparatedList( + VariableDeclarator(Identifier("value")) + .WithInitializer( + EqualsValueClause( + ConditionalExpression( + BinaryExpression( + SyntaxKind.EqualsExpression, + IdentifierName("Key"), + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("KeyEnum"), + IdentifierName("_Undefined") + ) + ), + LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(string.Empty)), + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("_resourceStringProvider"), + IdentifierName("GetString") + ) + ) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("Key"), + IdentifierName("ToString") + ) + ) + ) + ) + ) + ) + ) + ) + ) + )) + ), + // Return the value. If a Converter is set, call Converter.Convert. + ReturnStatement( + ConditionalExpression( + BinaryExpression( + SyntaxKind.IsExpression, + IdentifierName("Converter"), + LiteralExpression(SyntaxKind.NullLiteralExpression) + ), + IdentifierName("value"), + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("Converter"), + IdentifierName("Convert") + ) + ).WithArgumentList( + ArgumentList( + SeparatedList( + [ + Argument(IdentifierName("value")), + Argument(TypeOfExpression(PredefinedType(Token(SyntaxKind.StringKeyword)))), + Argument(IdentifierName("ConverterParameter")), + Argument(LiteralExpression(SyntaxKind.NullLiteralExpression)) + ]) + ) + ) + ) + ) + )); + + // Build the markup extension class. + var markupDecl = ClassDeclaration(className) + // Inherit from MarkupExtension. + .WithBaseList( + BaseList( + SingletonSeparatedList( + SimpleBaseType(ParseTypeName("MarkupExtension")) + ) + ) + ) + .WithAttributeLists(attributes) + .WithModifiers(TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.PartialKeyword) + )) + .AddMembers( + keyEnum, + resourceField, + staticCtor, + // Create the Key property. + PropertyDeclaration(ParseTypeName("KeyEnum"), "Key") + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) + .WithAccessorList( + AccessorList( + List( + [ + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + ]) + ) + ), + // Create the Converter property. + PropertyDeclaration(ParseTypeName("IValueConverter"), "Converter") + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) + .WithAccessorList( + AccessorList( + List( + [ + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + ]) + ) + ), + // Create the ConverterParameter property. + PropertyDeclaration(PredefinedType(Token(SyntaxKind.ObjectKeyword)), "ConverterParameter") + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) + .WithAccessorList( + AccessorList( + List( + [ + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + ]) + ) + ), + provideValueMethod + ); + + return markupDecl; + } +} \ No newline at end of file diff --git a/src/ReswPlus.SourceGenerator/Models/IProject.cs b/src/ReswPlus.SourceGenerator/Models/IProject.cs index 0ca2821..da7ba93 100644 --- a/src/ReswPlus.SourceGenerator/Models/IProject.cs +++ b/src/ReswPlus.SourceGenerator/Models/IProject.cs @@ -19,16 +19,4 @@ internal interface IProject /// Gets the programming language of the project. /// Language Language { get; } - - /// - /// Gets the precompiled header for the project. - /// - /// A string representing the precompiled header. - string GetPrecompiledHeader(); - - /// - /// Gets the indent string used in the project. - /// - /// A string representing the indent. - string GetIndentString(); } diff --git a/src/ReswPlus.SourceGenerator/Models/Project.cs b/src/ReswPlus.SourceGenerator/Models/Project.cs index 4d353b4..f46c674 100644 --- a/src/ReswPlus.SourceGenerator/Models/Project.cs +++ b/src/ReswPlus.SourceGenerator/Models/Project.cs @@ -19,22 +19,4 @@ internal sealed class Project(string name, bool isLibrary) : IProject /// Gets the language of the project, which is always C#. /// public Language Language => Language.CSharp; - - /// - /// Gets the indent string used in the project. - /// - /// A string containing two spaces. - public string GetIndentString() - { - return " "; - } - - /// - /// Gets the precompiled header for the project. - /// - /// An empty string. - public string GetPrecompiledHeader() - { - return ""; - } }