Initial Commit
This commit is contained in:
commit
7eb2e750e9
9 changed files with 1081 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.idea/
|
||||
.qodo/
|
||||
bin/
|
||||
obj/
|
||||
71
ActionGenerator.cs
Normal file
71
ActionGenerator.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace Godot.Sharp.Extended.Generators;
|
||||
|
||||
[Generator]
|
||||
public class ActionGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var projectFile = context.AdditionalTextsProvider
|
||||
.Where(file => file.Path.EndsWith("project.godot"))
|
||||
.Select((file, token) => file.GetText())
|
||||
.Collect();
|
||||
|
||||
context.RegisterSourceOutput(projectFile, GenerateCode);
|
||||
}
|
||||
|
||||
private void GenerateCode(SourceProductionContext ctx, ImmutableArray<SourceText> texts)
|
||||
{
|
||||
var memberCode = new StringBuilder();
|
||||
foreach (var text in texts)
|
||||
{
|
||||
if (text == null) continue;
|
||||
var content = text.ToString();
|
||||
var inputs = GetActions(content);
|
||||
var fields = inputs.Select(keyData => $$"""\tpublic static StringName {{keyData.ToPascalCase()}} = "{{keyData}}";""");
|
||||
|
||||
foreach (var field in fields) memberCode.AppendLine(field);
|
||||
}
|
||||
|
||||
var code = $$"""
|
||||
// <auto-generated />
|
||||
|
||||
using System;
|
||||
using Godot;
|
||||
|
||||
namespace Godot.Sharp.Extended;
|
||||
|
||||
public partial class Actions
|
||||
{
|
||||
{{memberCode}}
|
||||
}
|
||||
""";
|
||||
ctx.AddSource("Godot.Sharp.Extended.Actions.g.cs", SourceText.From(code, Encoding.UTF8));
|
||||
}
|
||||
|
||||
public List<string> GetActions(string content)
|
||||
{
|
||||
var results = new List<string>();
|
||||
var inputSection = false;
|
||||
foreach (var line in content.Split(['\n']))
|
||||
{
|
||||
if (line.Equals("[input]"))
|
||||
inputSection = true;
|
||||
else if (line.StartsWith("[") && inputSection) break;
|
||||
|
||||
if (!inputSection) continue;
|
||||
|
||||
var kv = line.Split(['='], 2);
|
||||
if (kv.Length == 2) results.Add(kv[0].Trim());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
24
Godot.Sharp.Extended.csproj
Normal file
24
Godot.Sharp.Extended.csproj
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<IsRoslynCompiler>true</IsRoslynCompiler>
|
||||
|
||||
<RootNamespace>Godot.Sharp.Extended</RootNamespace>
|
||||
<AssemblyName>Godot.Sharp.Extended</AssemblyName>
|
||||
<PackageId>Godot.Sharp.Extended</PackageId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
24
Godot.Sharp.Extended.sln
Normal file
24
Godot.Sharp.Extended.sln
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.2.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.Sharp.Extended", "Godot.Sharp.Extended.csproj", "{4D64B952-274C-14C1-D766-218DC5756B49}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{4D64B952-274C-14C1-D766-218DC5756B49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4D64B952-274C-14C1-D766-218DC5756B49}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4D64B952-274C-14C1-D766-218DC5756B49}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4D64B952-274C-14C1-D766-218DC5756B49}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {2935E47E-E0E5-4C65-9EE5-E6945505387D}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
168
NodeBindGenerator.cs
Normal file
168
NodeBindGenerator.cs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace Godot.Sharp.Extended.Generators;
|
||||
|
||||
[Generator]
|
||||
public class NodeBindGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeSourceCode = """
|
||||
// <auto-generated />
|
||||
namespace Godot.Sharp.Extended;
|
||||
|
||||
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field)]
|
||||
public class NodeBindAttribute : Attribute
|
||||
{
|
||||
}
|
||||
""";
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterPostInitializationOutput(ctx => ctx.AddSource(
|
||||
"NodeBindAttribute.g.cs",
|
||||
SourceText.From(AttributeSourceCode, Encoding.UTF8)));
|
||||
|
||||
var propertyProvider = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
predicate: (s, _) => s is ClassDeclarationSyntax,
|
||||
transform: (ctx, _) => GetDeclarationForSourceGen(ctx))
|
||||
.Where(t => t.attributeFound)
|
||||
.SelectMany((t, _) => t.Item1);
|
||||
|
||||
context.RegisterSourceOutput(context.CompilationProvider.Combine(propertyProvider.Collect()),
|
||||
(ctx, value) => GenerateCode(ctx, value.Left, value.Right));
|
||||
}
|
||||
|
||||
private (IEnumerable<MemberDeclarationSyntax>, bool attributeFound) GetDeclarationForSourceGen(
|
||||
GeneratorSyntaxContext context)
|
||||
{
|
||||
var classSyntax = (ClassDeclarationSyntax)context.Node;
|
||||
var members = classSyntax.Members;
|
||||
var foundMembers = members.Select(member => IsNodeBindAttributeOnMember(context.SemanticModel, member))
|
||||
.Where(syntax => syntax is not null)
|
||||
.ToList();
|
||||
return (foundMembers!, foundMembers.Count > 0);
|
||||
}
|
||||
|
||||
private MemberDeclarationSyntax? IsNodeBindAttributeOnMember(SemanticModel model,
|
||||
MemberDeclarationSyntax declarationSyntax)
|
||||
{
|
||||
foreach (var attributeSyntax in declarationSyntax
|
||||
.AttributeLists
|
||||
.SelectMany(attributeList => attributeList.Attributes))
|
||||
{
|
||||
if (model.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol symbol)
|
||||
continue;
|
||||
|
||||
var attributeName = symbol.ContainingType.ToDisplayString();
|
||||
if (attributeName == $"Godot.Sharp.Extended.NodeBind")
|
||||
return declarationSyntax;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void GenerateCode(SourceProductionContext ctx, Compilation compilation,
|
||||
ImmutableArray<MemberDeclarationSyntax> members)
|
||||
{
|
||||
Dictionary<string, ClassDefinition> classDefinitions = [];
|
||||
|
||||
foreach (var member in members)
|
||||
{
|
||||
if (member.Parent is ClassDeclarationSyntax classDeclaration)
|
||||
{
|
||||
var semanticModel = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
|
||||
if (semanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol classSymbol)
|
||||
continue;
|
||||
|
||||
var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
|
||||
var className = classDeclaration.Identifier.Text;
|
||||
var key = $"{namespaceName}.{className}";
|
||||
if (!classDefinitions.ContainsKey(key)) classDefinitions[key] = new ClassDefinition();
|
||||
|
||||
var classDefinition = classDefinitions[key];
|
||||
classDefinition.ClassName = className;
|
||||
classDefinition.Namespace = namespaceName;
|
||||
if (member is FieldDeclarationSyntax field)
|
||||
{
|
||||
var variable = field.Declaration.Variables.First();
|
||||
|
||||
if (semanticModel.GetDeclaredSymbol(variable) is not IFieldSymbol fieldSymbol) continue;
|
||||
|
||||
var memberDefinition = new MemberDefinition
|
||||
{
|
||||
Name = fieldSymbol.Name,
|
||||
Type = fieldSymbol.Type.Name
|
||||
};
|
||||
classDefinition.MemberDefinitions.Add(memberDefinition);
|
||||
classDefinition.UsingNamespaceName.Add(fieldSymbol.Type.ContainingNamespace.ToDisplayString());
|
||||
}
|
||||
else if (member is PropertyDeclarationSyntax property)
|
||||
{
|
||||
if (semanticModel.GetDeclaredSymbol(property) is not IPropertySymbol propertySymbol) continue;
|
||||
|
||||
var memberDefinition = new MemberDefinition
|
||||
{
|
||||
Name = propertySymbol.Name,
|
||||
Type = propertySymbol.Type.Name
|
||||
};
|
||||
classDefinition.MemberDefinitions.Add(memberDefinition);
|
||||
classDefinition.UsingNamespaceName.Add(propertySymbol.Type.ContainingNamespace.ToDisplayString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var definition in classDefinitions.Values) GenerateClass(ctx, definition);
|
||||
}
|
||||
|
||||
private void GenerateClass(SourceProductionContext ctx, ClassDefinition definition)
|
||||
{
|
||||
var memberCode = new StringBuilder();
|
||||
foreach (var memberDefinition in definition.MemberDefinitions)
|
||||
{
|
||||
var nodeName = memberDefinition.Name.TrimStart('_').ToPascalCase();
|
||||
|
||||
memberCode.AppendLine($$"""
|
||||
{{memberDefinition.Name}} = GetNode<{{memberDefinition.Type}}>("%{{nodeName}}");
|
||||
""");
|
||||
}
|
||||
|
||||
var usingCode = new StringBuilder();
|
||||
foreach (var namespaceName in definition.UsingNamespaceName) usingCode.AppendLine($"using {namespaceName};");
|
||||
|
||||
var code = $$"""
|
||||
// <auto-generated/>
|
||||
|
||||
using System;
|
||||
{{usingCode}}
|
||||
|
||||
namespace {{definition.Namespace}};
|
||||
|
||||
partial class {{definition.ClassName}}
|
||||
{
|
||||
private void BindNodes() {
|
||||
{{memberCode}}
|
||||
}
|
||||
}
|
||||
""";
|
||||
ctx.AddSource($"Godot.Sharp.Extended.{definition.ClassName}.NodeBind.g.cs", SourceText.From(code, Encoding.UTF8));
|
||||
}
|
||||
|
||||
private class ClassDefinition
|
||||
{
|
||||
public string Namespace { get; set; } = "";
|
||||
public string ClassName { get; set; } = "";
|
||||
public HashSet<string> UsingNamespaceName { get; set; } = [];
|
||||
public List<MemberDefinition> MemberDefinitions { get; } = [];
|
||||
}
|
||||
|
||||
private class MemberDefinition
|
||||
{
|
||||
public string Type { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
}
|
||||
}
|
||||
180
NodePropBindGenerator.cs
Normal file
180
NodePropBindGenerator.cs
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace Godot.Sharp.Extended.Generators;
|
||||
|
||||
[Generator]
|
||||
public class NodePropBindGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeSourceCode = """
|
||||
// <auto-generated/>
|
||||
namespace Godot.Sharp.Extended;
|
||||
|
||||
[System.AttributeUsage(System.AttributeTargets.Member)]
|
||||
public class NodePropBindAttribute : System.Attribute
|
||||
{
|
||||
public string TargetNodeName { get; private set; }
|
||||
public string GodotPropertyName { get; private set; }
|
||||
|
||||
public NodePropBindAttribute(string targetNodeName, string godotPropertyName)
|
||||
{
|
||||
TargetNodeName = targetNodeName;
|
||||
GodotPropertyName = godotPropertyName;
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterPostInitializationOutput(ctx => ctx.AddSource(
|
||||
"NodePropBind.g.cs",
|
||||
SourceText.From(AttributeSourceCode, Encoding.UTF8)));
|
||||
|
||||
var propertyProvider = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
predicate: (s, _) => s is ClassDeclarationSyntax,
|
||||
transform: (ctx, _) => GetDeclarationForSourceGen(ctx))
|
||||
.Where(t => t.attributeFound)
|
||||
.SelectMany((t, _) => t.members);
|
||||
|
||||
context.RegisterSourceOutput(context.CompilationProvider.Combine(propertyProvider.Collect()),
|
||||
(ctx, value) => GenerateCode(ctx, value.Left, value.Right));
|
||||
}
|
||||
|
||||
private (IEnumerable<MemberDeclarationSyntax> members, bool attributeFound) GetDeclarationForSourceGen(
|
||||
GeneratorSyntaxContext context)
|
||||
{
|
||||
var classSyntax = (ClassDeclarationSyntax)context.Node;
|
||||
var members = classSyntax.Members;
|
||||
var foundMembers = members.Select(member => IsNodePropBindAttributeOnMember(context.SemanticModel, member))
|
||||
.Where(syntax => syntax is not null)
|
||||
.ToList();
|
||||
return (foundMembers!, foundMembers.Count > 0);
|
||||
}
|
||||
|
||||
private MemberDeclarationSyntax? IsNodePropBindAttributeOnMember(SemanticModel model,
|
||||
MemberDeclarationSyntax declarationSyntax)
|
||||
{
|
||||
foreach (var attributeSyntax in declarationSyntax
|
||||
.AttributeLists
|
||||
.SelectMany(attributeList => attributeList.Attributes))
|
||||
{
|
||||
if (model.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol symbol)
|
||||
continue;
|
||||
|
||||
var attributeName = symbol.ContainingType.ToDisplayString();
|
||||
if (attributeName == "Godot.Sharp.Extended.NodePropBind")
|
||||
return declarationSyntax;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void GenerateCode(SourceProductionContext context, Compilation compilation,
|
||||
ImmutableArray<MemberDeclarationSyntax> members)
|
||||
{
|
||||
Dictionary<string, ClassDefinition> classDefinitions = [];
|
||||
|
||||
foreach (var member in members)
|
||||
{
|
||||
if (member.Parent is ClassDeclarationSyntax classDeclaration)
|
||||
{
|
||||
var semanticModel = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
|
||||
if (semanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol classSymbol)
|
||||
continue;
|
||||
|
||||
var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
|
||||
var className = classDeclaration.Identifier.Text;
|
||||
var key = $"{namespaceName}.{className}";
|
||||
if (!classDefinitions.ContainsKey(key))
|
||||
classDefinitions[key] = new ClassDefinition(namespaceName, className, new HashSet<string>(),
|
||||
new List<MemberDefinition>());
|
||||
|
||||
var classDefinition = classDefinitions[key];
|
||||
if (member is FieldDeclarationSyntax field)
|
||||
{
|
||||
var variable = field.Declaration.Variables.First();
|
||||
|
||||
if (semanticModel.GetDeclaredSymbol(variable) is not IFieldSymbol fieldSymbol) continue;
|
||||
|
||||
var attributeData = fieldSymbol.GetAttributes()
|
||||
.FirstOrDefault(ad => ad.AttributeClass?.ToDisplayString() == "Godot.Sharp.Extended.NodePropBindAttribute");
|
||||
|
||||
if (attributeData is null) continue;
|
||||
|
||||
var nodeProp = attributeData.ConstructorArguments[0].Value as string;
|
||||
var godotProp = attributeData.ConstructorArguments[1].Value as string;
|
||||
|
||||
var memberDefinition = new MemberDefinition(fieldSymbol.Type.Name, fieldSymbol.Name, nodeProp, godotProp);
|
||||
classDefinition.MemberDefinitions.Add(memberDefinition);
|
||||
classDefinition.UsingNamespaceName.Add(fieldSymbol.ContainingNamespace.ToDisplayString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var definition in classDefinitions.Values) GenerateClass(context, definition);
|
||||
}
|
||||
|
||||
private void GenerateClass(SourceProductionContext context, ClassDefinition definition)
|
||||
{
|
||||
var propCode = new StringBuilder();
|
||||
foreach (var memberDefinition in definition.MemberDefinitions)
|
||||
{
|
||||
var propName = memberDefinition.Name.TrimStart('_').ToPascalCase();
|
||||
var godotProp = memberDefinition.GodotProp.ToPascalCase();
|
||||
|
||||
propCode.AppendLine($$"""
|
||||
public {{memberDefinition.Type}} {{propName}}
|
||||
{
|
||||
get => {{memberDefinition.Name}};
|
||||
set {
|
||||
{{memberDefinition.Name}} = value;
|
||||
if ({{memberDefinition.NodeProp}} != null)
|
||||
{{memberDefinition.NodeProp}}.{{godotProp}} = value;
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
var usingCode = new StringBuilder();
|
||||
foreach (var namespaceName in definition.UsingNamespaceName) usingCode.AppendLine($"using {namespaceName};");
|
||||
|
||||
var code = $$"""
|
||||
// <auto-generated />
|
||||
|
||||
using System;
|
||||
using Godot;
|
||||
{{usingCode}}
|
||||
|
||||
namespace {{definition.Namespace}};
|
||||
|
||||
partial class {{definition.ClassName}}
|
||||
{
|
||||
{{propCode}}
|
||||
}
|
||||
""";
|
||||
context.AddSource($"Godot.Sharp.Extended.{definition.ClassName}.NodeBind.g.cs", SourceText.From(code, Encoding.UTF8));
|
||||
}
|
||||
|
||||
private class ClassDefinition(string theNamespace, string className, HashSet<string> usingNamespaceName, List<MemberDefinition> memberDefinitions)
|
||||
{
|
||||
public string Namespace => theNamespace;
|
||||
public string ClassName => className;
|
||||
public HashSet<string> UsingNamespaceName => usingNamespaceName;
|
||||
public List<MemberDefinition> MemberDefinitions => memberDefinitions;
|
||||
}
|
||||
|
||||
private class MemberDefinition(string type, string name, string nodeProp, string godotProp)
|
||||
{
|
||||
public string Type => type;
|
||||
public string Name => name;
|
||||
public string NodeProp => nodeProp;
|
||||
public string GodotProp => godotProp;
|
||||
}
|
||||
}
|
||||
241
ResourceGenerator.cs
Normal file
241
ResourceGenerator.cs
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace Godot.Sharp.Extended.Generators;
|
||||
|
||||
[Generator]
|
||||
public class ResourceGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeSourceCode = """
|
||||
// <auto-generated />
|
||||
namespace Godot.Sharp.Extended;
|
||||
|
||||
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field)]
|
||||
public class ResourcePathAttribute : System.Attribute
|
||||
{
|
||||
public string Path { get; private set; }
|
||||
|
||||
public ResourcePathAttribute(string path) {
|
||||
Path = path;
|
||||
}
|
||||
}
|
||||
""";
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterPostInitializationOutput(ctx => ctx.AddSource(
|
||||
"ResourcePathAttribute.g.cs",
|
||||
SourceText.From(AttributeSourceCode, Encoding.UTF8)));
|
||||
|
||||
var classPaths = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
predicate: (s, _) => s is ClassDeclarationSyntax syntax,
|
||||
transform: (ctx, _) => GetClassDeclaration(ctx))
|
||||
.Where(t => t.reportAttributeFound)
|
||||
.Select((t, _) => (t.Item1, t.path))
|
||||
.Collect();
|
||||
|
||||
var provider2 = context.AdditionalTextsProvider.Combine(classPaths)
|
||||
.Where(CheckResourcePath)
|
||||
.Select((t1, _) =>
|
||||
{
|
||||
var classDeclarationForPath = GetClassDeclarationForPath(t1);
|
||||
return new Storage(t1.Left, classDeclarationForPath.Item1, classDeclarationForPath.path);
|
||||
});
|
||||
|
||||
context.RegisterSourceOutput(context.CompilationProvider.Combine(provider2.Collect()),
|
||||
(ctx, t) => GenerateCode(ctx, t.Left, t.Right));
|
||||
}
|
||||
|
||||
private (ClassDeclarationSyntax, string path) GetClassDeclarationForPath((AdditionalText file,
|
||||
ImmutableArray<(ClassDeclarationSyntax, string path)> ClassDeclarationSyntax) tuple)
|
||||
{
|
||||
var realFilePath = tuple.file.Path;
|
||||
var relativeFilePath = Path.GetDirectoryName(realFilePath)?.Replace('\\', '/');
|
||||
return tuple.ClassDeclarationSyntax.First(data => relativeFilePath!.EndsWith(data.path));
|
||||
}
|
||||
|
||||
private bool CheckResourcePath(
|
||||
(AdditionalText file, ImmutableArray<(ClassDeclarationSyntax, string path)> Right) tuple)
|
||||
{
|
||||
var realFilePath = tuple.file.Path;
|
||||
var filePathToCheck = tuple.Right.Select(data => data.path);
|
||||
var relativeFilePath = Path.GetDirectoryName(realFilePath)?.Replace('\\', '/');
|
||||
return filePathToCheck.Any(path => relativeFilePath?.EndsWith(path) ?? false);
|
||||
}
|
||||
|
||||
private (ClassDeclarationSyntax classDeclarationSyntax, bool reportAttributeFound, string path) GetClassDeclaration(
|
||||
GeneratorSyntaxContext ctx)
|
||||
{
|
||||
var classDeclarationSyntax = (ClassDeclarationSyntax)ctx.Node;
|
||||
|
||||
foreach (var attributeSyntax in classDeclarationSyntax
|
||||
.AttributeLists
|
||||
.SelectMany(attributeListSyntax => attributeListSyntax.Attributes))
|
||||
{
|
||||
var symbolInfo = ctx.SemanticModel.GetSymbolInfo(attributeSyntax);
|
||||
if (symbolInfo.Symbol is not IMethodSymbol attributeSymbol) continue;
|
||||
if (attributeSymbol.ContainingType.ToDisplayString() != "Godot.Sharp.Extended.ResourcePath") continue;
|
||||
var parameter = attributeSyntax.ArgumentList;
|
||||
var pathArgument = parameter?.Arguments[0];
|
||||
|
||||
var path = "";
|
||||
if (pathArgument?.Expression is LiteralExpressionSyntax literal) path = literal.Token.ValueText;
|
||||
|
||||
return (classDeclarationSyntax, true, path);
|
||||
}
|
||||
|
||||
return (classDeclarationSyntax, false, "");
|
||||
}
|
||||
|
||||
private void GenerateCode(
|
||||
SourceProductionContext context,
|
||||
Compilation compilation,
|
||||
IEnumerable<Storage> storages)
|
||||
{
|
||||
Dictionary<string, ClassDefinition> classDefinitions = new();
|
||||
|
||||
foreach (var storage in storages)
|
||||
{
|
||||
var path = storage.Path;
|
||||
var text = storage.Text;
|
||||
var classDeclarationSyntax = storage.ClassDeclarationSyntax;
|
||||
|
||||
var firstLine = text.GetText()?.Lines[0].ToString();
|
||||
if (firstLine == null) continue;
|
||||
var scriptNameMatch = Regex.Match(firstLine, @"script_class=""(.+?)""");
|
||||
if (!scriptNameMatch.Success) scriptNameMatch = Regex.Match(firstLine, @"type=""(.+?)""");
|
||||
|
||||
var scriptName = scriptNameMatch.Groups[1].Value;
|
||||
var fileName = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
var scriptNamespaceType = FindClassSymbol(compilation.GlobalNamespace, scriptName);
|
||||
var scriptNamespace = scriptNamespaceType?.ContainingNamespace?.ToDisplayString();
|
||||
|
||||
var semanticModel = compilation.GetSemanticModel(classDeclarationSyntax.SyntaxTree);
|
||||
if (semanticModel.GetDeclaredSymbol(classDeclarationSyntax) is not INamedTypeSymbol classSymbol) continue;
|
||||
|
||||
var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
|
||||
var className = classDeclarationSyntax.Identifier.Text;
|
||||
|
||||
var key = $"{namespaceName}.{className}";
|
||||
if (!classDefinitions.ContainsKey(key))
|
||||
classDefinitions[key] = new ClassDefinition
|
||||
{
|
||||
ClassName = className,
|
||||
NamespaceName = namespaceName,
|
||||
};
|
||||
|
||||
var def = classDefinitions[key];
|
||||
def.Resources.Add(new ResourceDefinition
|
||||
{
|
||||
Type = scriptName,
|
||||
FieldName = fileName,
|
||||
FilePath = path + "/" + fileName + ".tres"
|
||||
});
|
||||
|
||||
if (scriptNamespace != null) def.UsingDefinitionSet.Add(scriptNamespace);
|
||||
}
|
||||
|
||||
foreach (var classDefinition in classDefinitions.Values)
|
||||
{
|
||||
var resources = new StringBuilder();
|
||||
var allDefinition = new StringBuilder();
|
||||
var usingDefinition = new StringBuilder();
|
||||
|
||||
foreach(var def in classDefinition.UsingDefinitionSet) usingDefinition.AppendLine($"using {def};");
|
||||
|
||||
if (!classDefinition.UsingDefinitionSet.Contains("Godot")) usingDefinition.AppendLine("using Godot;");
|
||||
|
||||
foreach(var resource in classDefinition.Resources)
|
||||
resources.AppendLine(
|
||||
$"""
|
||||
public static readonly {resource.Type} {resource.FieldName} =
|
||||
GD.Load<{resource.Type}>(ProjectSettings.GlobalizePath("res://{resource.FilePath}"));
|
||||
""");
|
||||
|
||||
var resourceType = "";
|
||||
var generateAll = true;
|
||||
foreach (var resource in classDefinition.Resources)
|
||||
{
|
||||
if (resourceType != "" && resourceType != resource.Type) generateAll = false;
|
||||
|
||||
resourceType = resource.Type;
|
||||
allDefinition.AppendLine(
|
||||
$" {resource.FieldName},");
|
||||
}
|
||||
|
||||
var allMethod = "";
|
||||
if (generateAll)
|
||||
allMethod =
|
||||
$"""
|
||||
public static {resourceType}[] All => [
|
||||
{allDefinition}];
|
||||
""";
|
||||
else
|
||||
allMethod =
|
||||
$"""
|
||||
public static Resource[] All => [
|
||||
{allDefinition}];
|
||||
""";
|
||||
|
||||
var code =
|
||||
$$"""
|
||||
// <auto-generated />
|
||||
using System;
|
||||
{{usingDefinition}}
|
||||
|
||||
namespace {{classDefinition.NamespaceName}};
|
||||
|
||||
public partial class {{classDefinition.ClassName}}
|
||||
{
|
||||
{{resources}}
|
||||
{{allMethod}}
|
||||
}
|
||||
""";
|
||||
context.AddSource($"Godot.Sharp.Extended.{classDefinition.ClassName}.Res.g.cs", SourceText.From(code, Encoding.UTF8));
|
||||
}
|
||||
}
|
||||
|
||||
private INamedTypeSymbol? FindClassSymbol(INamespaceSymbol namespaceSymbol, string className)
|
||||
{
|
||||
var classSymbol = namespaceSymbol.GetTypeMembers().FirstOrDefault(m => m.Name == className);
|
||||
if (classSymbol != null) return classSymbol;
|
||||
|
||||
foreach (var childNamespace in namespaceSymbol.GetNamespaceMembers())
|
||||
{
|
||||
classSymbol = FindClassSymbol(childNamespace, className);
|
||||
if (classSymbol != null) return classSymbol;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private class ClassDefinition
|
||||
{
|
||||
public HashSet<string> UsingDefinitionSet { get; private set; } = [];
|
||||
public string NamespaceName { get; set; } = "";
|
||||
public string ClassName { get; set; } = "";
|
||||
public List<ResourceDefinition> Resources { get; private set; } = [];
|
||||
}
|
||||
|
||||
private class ResourceDefinition
|
||||
{
|
||||
public string Type { get; set; } = "";
|
||||
public string FilePath { get; set; } = "";
|
||||
public string FieldName { get; set; } = "";
|
||||
}
|
||||
|
||||
private class Storage(AdditionalText text, ClassDeclarationSyntax classDeclarationSyntax, string path)
|
||||
{
|
||||
public AdditionalText Text => text;
|
||||
public ClassDeclarationSyntax ClassDeclarationSyntax => classDeclarationSyntax;
|
||||
public string Path => path;
|
||||
}
|
||||
}
|
||||
64
SceneGenerator.cs
Normal file
64
SceneGenerator.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace Godot.Sharp.Extended.Generators;
|
||||
|
||||
[Generator]
|
||||
public class SceneGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var scenes = context.AdditionalTextsProvider
|
||||
.Where(text => text.Path.EndsWith(".tscn"));
|
||||
|
||||
context.RegisterSourceOutput(
|
||||
context.CompilationProvider.Combine(scenes.Collect()).Combine(context.AnalyzerConfigOptionsProvider),
|
||||
(ctx, t) => GenerateCode(ctx, t.Left.Left, t.Left.Right, t.Right));
|
||||
}
|
||||
|
||||
private void GenerateCode(
|
||||
SourceProductionContext context,
|
||||
Compilation compilation,
|
||||
IEnumerable<AdditionalText> scenes,
|
||||
AnalyzerConfigOptionsProvider optionsProvider)
|
||||
{
|
||||
optionsProvider.GlobalOptions.TryGetValue("build_property.projectdir", out var projectDirectory);
|
||||
|
||||
var allMethods = new StringBuilder();
|
||||
foreach (var scene in scenes)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(scene.Path);
|
||||
var name = Path.GetFileNameWithoutExtension(scene.Path);
|
||||
var relativePath = scene.Path.Replace(projectDirectory ?? "", "").Replace("\\", "/");
|
||||
allMethods.AppendLine(
|
||||
$$"""
|
||||
public static T New{{name.ToPascalCase()}}<T>() where T : Node {
|
||||
var packedScene = GD.Load<PackedScene>("res://{{relativePath}}");
|
||||
return packedScene.Instance<T>();
|
||||
}
|
||||
""");
|
||||
|
||||
var code =
|
||||
$$"""
|
||||
// <auto-generated />
|
||||
|
||||
using System;
|
||||
using Godot;
|
||||
|
||||
namespace Godot.Sharp.Extended;
|
||||
|
||||
public class Scenes
|
||||
{
|
||||
{{allMethods}}
|
||||
}
|
||||
""";
|
||||
|
||||
context.AddSource("Godot.Sharp.Extended.Scenes.g.cs", SourceText.From(code, Encoding.UTF8));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
305
StringExtensionMethods.cs
Normal file
305
StringExtensionMethods.cs
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Godot.Sharp.Extended.Generators;
|
||||
|
||||
public static class StringExtensionMethods
|
||||
{
|
||||
public static string ToPascalCase(this string value)
|
||||
{
|
||||
int newIndex = 0;
|
||||
bool insertSeparator = true;
|
||||
UnicodeCategory previous;
|
||||
UnicodeCategory current = UnicodeCategory.OtherSymbol;
|
||||
char[] newString = new char[value.Length + CalculateSpanSizeForPascalOrCamelCase(value)];
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
previous = current;
|
||||
current = char.GetUnicodeCategory(value[i]);
|
||||
insertSeparator =
|
||||
(previous != current && (current is UnicodeCategory.UppercaseLetter ||
|
||||
current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator;
|
||||
if (!IsSpecialCharacter(current))
|
||||
{
|
||||
newString[newIndex] = insertSeparator
|
||||
? char.ToUpperInvariant(value[i])
|
||||
: char.ToLowerInvariant(value[i]);
|
||||
insertSeparator = false;
|
||||
newIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return new(newString);
|
||||
}
|
||||
|
||||
public static string ToCamelCase(this string value)
|
||||
{
|
||||
int newIndex = 0;
|
||||
bool insertSeparator = false;
|
||||
bool isFirstCharacter = true;
|
||||
UnicodeCategory previous;
|
||||
UnicodeCategory current = UnicodeCategory.OtherSymbol;
|
||||
char[] newString = new char[value.Length + CalculateSpanSizeForPascalOrCamelCase(value)];
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
previous = current;
|
||||
current = char.GetUnicodeCategory(value[i]);
|
||||
insertSeparator =
|
||||
(previous != current && (current is UnicodeCategory.UppercaseLetter ||
|
||||
current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator;
|
||||
if (!IsSpecialCharacter(current))
|
||||
{
|
||||
newString[newIndex] = insertSeparator && !isFirstCharacter
|
||||
? char.ToUpperInvariant(value[i])
|
||||
: char.ToLowerInvariant(value[i]);
|
||||
isFirstCharacter = false;
|
||||
insertSeparator = false;
|
||||
newIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return new(newString);
|
||||
}
|
||||
|
||||
public static string ToUnderscoreCamelCase(this string value)
|
||||
{
|
||||
int newIndex = 1;
|
||||
bool insertSeparator = false;
|
||||
bool isFirstCharacter = true;
|
||||
UnicodeCategory previous;
|
||||
UnicodeCategory current = UnicodeCategory.OtherSymbol;
|
||||
char[] newString = new char[value.Length + CalculateSpanSizeForPascalOrCamelCase(value) + 1];
|
||||
newString[0] = '_';
|
||||
for (int i = 1; i < value.Length; i++)
|
||||
{
|
||||
previous = current;
|
||||
current = char.GetUnicodeCategory(value[i]);
|
||||
insertSeparator =
|
||||
(previous != current && (current is UnicodeCategory.UppercaseLetter ||
|
||||
current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator;
|
||||
if (!IsSpecialCharacter(current))
|
||||
{
|
||||
newString[newIndex] = insertSeparator && !isFirstCharacter
|
||||
? char.ToUpperInvariant(value[i])
|
||||
: char.ToLowerInvariant(value[i]);
|
||||
isFirstCharacter = false;
|
||||
insertSeparator = false;
|
||||
newIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return new(newString);
|
||||
}
|
||||
|
||||
public static string ToKebabCase(this string value)
|
||||
{
|
||||
int newIndex = 0;
|
||||
bool insertSeparator = true;
|
||||
bool isFirstCharacter = true;
|
||||
UnicodeCategory previous;
|
||||
UnicodeCategory current = UnicodeCategory.OtherSymbol;
|
||||
char[] newString = new char[value.Length + CalculateSpanSizeForKebabOrSnakeCase(value)];
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
previous = current;
|
||||
current = char.GetUnicodeCategory(value[i]);
|
||||
insertSeparator =
|
||||
(previous != current && (current is UnicodeCategory.UppercaseLetter ||
|
||||
current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator;
|
||||
if (!IsSpecialCharacter(current))
|
||||
{
|
||||
if (insertSeparator && !isFirstCharacter)
|
||||
{
|
||||
newString[newIndex] = '-';
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
newString[newIndex] = char.ToLowerInvariant(value[i]);
|
||||
isFirstCharacter = false;
|
||||
insertSeparator = false;
|
||||
newIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return new(newString);
|
||||
}
|
||||
|
||||
public static string ToSnakeCase(this string value)
|
||||
{
|
||||
int newIndex = 0;
|
||||
bool insertSeparator = true;
|
||||
bool isFirstCharacter = true;
|
||||
UnicodeCategory previous;
|
||||
UnicodeCategory current = UnicodeCategory.OtherSymbol;
|
||||
char[] newString = new char[value.Length + CalculateSpanSizeForKebabOrSnakeCase(value)];
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
previous = current;
|
||||
current = char.GetUnicodeCategory(value[i]);
|
||||
insertSeparator =
|
||||
(previous != current && (current is UnicodeCategory.UppercaseLetter ||
|
||||
current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator;
|
||||
if (!IsSpecialCharacter(current))
|
||||
{
|
||||
if (insertSeparator && !isFirstCharacter)
|
||||
{
|
||||
newString[newIndex] = '_';
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
newString[newIndex] = char.ToLowerInvariant(value[i]);
|
||||
isFirstCharacter = false;
|
||||
insertSeparator = false;
|
||||
newIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return new(newString);
|
||||
}
|
||||
|
||||
public static string ToMacroCase(this string value)
|
||||
{
|
||||
int newIndex = 0;
|
||||
bool insertSeparator = true;
|
||||
bool isFirstCharacter = true;
|
||||
UnicodeCategory previous;
|
||||
UnicodeCategory current = UnicodeCategory.OtherSymbol;
|
||||
char[] newString = new char[value.Length + CalculateSpanSizeForKebabOrSnakeCase(value)];
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
previous = current;
|
||||
current = char.GetUnicodeCategory(value[i]);
|
||||
insertSeparator =
|
||||
(previous != current && (current is UnicodeCategory.UppercaseLetter ||
|
||||
current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator;
|
||||
if (!IsSpecialCharacter(current))
|
||||
{
|
||||
if (insertSeparator && !isFirstCharacter)
|
||||
{
|
||||
newString[newIndex] = '_';
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
newString[newIndex] = char.ToUpperInvariant(value[i]);
|
||||
isFirstCharacter = false;
|
||||
insertSeparator = false;
|
||||
newIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return new(newString);
|
||||
}
|
||||
|
||||
public static string ToTrainCase(this string value)
|
||||
{
|
||||
int newIndex = 0;
|
||||
bool insertSeparator = true;
|
||||
bool isFirstCharacter = true;
|
||||
UnicodeCategory previous;
|
||||
UnicodeCategory current = UnicodeCategory.OtherSymbol;
|
||||
char[] newString = new char[value.Length + CalculateSpanSizeForKebabOrSnakeCase(value)];
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
previous = current;
|
||||
current = char.GetUnicodeCategory(value[i]);
|
||||
insertSeparator =
|
||||
(previous != current && (current is UnicodeCategory.UppercaseLetter ||
|
||||
current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator;
|
||||
if (!IsSpecialCharacter(current))
|
||||
{
|
||||
if (insertSeparator && !isFirstCharacter)
|
||||
{
|
||||
newString[newIndex] = '-';
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
newString[newIndex] = insertSeparator
|
||||
? char.ToUpperInvariant(value[i])
|
||||
: char.ToLowerInvariant(value[i]);
|
||||
isFirstCharacter = false;
|
||||
insertSeparator = false;
|
||||
newIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return new(newString);
|
||||
}
|
||||
|
||||
public static string ToTitleCase(this string value)
|
||||
{
|
||||
int newIndex = 0;
|
||||
bool insertSeparator = true;
|
||||
bool isFirstCharacter = true;
|
||||
UnicodeCategory previous;
|
||||
UnicodeCategory current = UnicodeCategory.OtherSymbol;
|
||||
char[] newString = new char[value.Length + CalculateSpanSizeForKebabOrSnakeCase(value)];
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
previous = current;
|
||||
current = char.GetUnicodeCategory(value[i]);
|
||||
insertSeparator =
|
||||
(previous != current && (current is UnicodeCategory.UppercaseLetter ||
|
||||
current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator;
|
||||
if (!IsSpecialCharacter(current))
|
||||
{
|
||||
if (insertSeparator && !isFirstCharacter)
|
||||
{
|
||||
newString[newIndex] = ' ';
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
newString[newIndex] = insertSeparator
|
||||
? char.ToUpperInvariant(value[i])
|
||||
: char.ToLowerInvariant(value[i]);
|
||||
isFirstCharacter = false;
|
||||
insertSeparator = false;
|
||||
newIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return new(newString);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int CalculateSpanSizeForKebabOrSnakeCase(string text)
|
||||
{
|
||||
UnicodeCategory previous = char.GetUnicodeCategory(text[0]);
|
||||
UnicodeCategory current;
|
||||
int skips = IsSpecialCharacter(previous) ? 1 : 0;
|
||||
int divs = 0;
|
||||
for (int i = 1; i < text.Length; i++)
|
||||
{
|
||||
current = char.GetUnicodeCategory(text[i]);
|
||||
skips += IsSpecialCharacter(current) ? 1 : 0;
|
||||
divs += previous != current && (current is UnicodeCategory.UppercaseLetter ||
|
||||
current is UnicodeCategory.DecimalDigitNumber)
|
||||
? 1
|
||||
: 0;
|
||||
previous = current;
|
||||
}
|
||||
|
||||
return divs - skips;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int CalculateSpanSizeForPascalOrCamelCase(string text)
|
||||
{
|
||||
UnicodeCategory current;
|
||||
int skips = 0;
|
||||
for (int i = 0; i < text.Length - 1; i++)
|
||||
{
|
||||
current = char.GetUnicodeCategory(text[i]);
|
||||
skips -= IsSpecialCharacter(current) ? 1 : 0;
|
||||
}
|
||||
|
||||
return skips;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsSpecialCharacter(UnicodeCategory category)
|
||||
{
|
||||
return category is not UnicodeCategory.UppercaseLetter
|
||||
and not UnicodeCategory.LowercaseLetter
|
||||
and not UnicodeCategory.DecimalDigitNumber;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue