diff --git a/docs/Value-Forms.md b/docs/Value-Forms.md index 0e7920920e3..75eb131fd5d 100644 --- a/docs/Value-Forms.md +++ b/docs/Value-Forms.md @@ -156,4 +156,13 @@ When the value say, `Test ©` is supplied as the value for the parameter `exampl "identifier": "kebabCase" } } -``` \ No newline at end of file +``` + +**`snakeCase`** - Converts the value to snake case using the casing rules of the invariant culture. Available since .NET 9.0.100. +```json +"forms": { + "snakeCase": { + "identifier": "snakeCase" + } +} +``` diff --git a/dotnet-template-samples/content/16-string-value-transform/MyProject.Con/.template.config/template.json b/dotnet-template-samples/content/16-string-value-transform/MyProject.Con/.template.config/template.json index 4b24b0ae522..e456b7af35e 100644 --- a/dotnet-template-samples/content/16-string-value-transform/MyProject.Con/.template.config/template.json +++ b/dotnet-template-samples/content/16-string-value-transform/MyProject.Con/.template.config/template.json @@ -36,6 +36,12 @@ "valueSource": "pascalCasedName", "replaces": "John Smith (kebab-case)", "valueTransform": "kebabCase" + }, + "snakeCasedName": { + "type": "derived", + "valueSource": "snakeCasedName", + "replaces": "John Smith (snake_case)", + "valueTransform": "snakeCase" } }, "forms" :{ @@ -57,8 +63,11 @@ "kebabCase": { "identifier": "kebabCase" }, + "snakeCase": { + "identifier": "snakeCase" + }, "firtstLowerCase": { "identifier": "firstLowerCaseInvariant" } } -} \ No newline at end of file +} diff --git a/dotnet-template-samples/content/16-string-value-transform/README.md b/dotnet-template-samples/content/16-string-value-transform/README.md index 7dc93ce828a..0939d2e1da6 100644 --- a/dotnet-template-samples/content/16-string-value-transform/README.md +++ b/dotnet-template-samples/content/16-string-value-transform/README.md @@ -14,7 +14,7 @@ See Details - A `derived` `type` with a value transformation is performed using [value forms](https://github.com/dotnet/templating/blob/main/docs/Value-Forms.md) (`ValueForms` type). - - The sample uses `replace`, `titleCase`, `kebabCase`, `firstLowerCaseInvariant` and `chain` value forms. + - The sample uses `replace`, `titleCase`, `kebabCase`, `snakeCase`, `firstLowerCaseInvariant` and `chain` value forms. - More value forms can be found in [the source code](https://github.com/dotnet/templating/tree/main/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ValueForms). Related diff --git a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Schemas/JSON/template.json b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Schemas/JSON/template.json index 292587dcc4e..1ce508a96d1 100644 --- a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Schemas/JSON/template.json +++ b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Schemas/JSON/template.json @@ -877,7 +877,8 @@ "firstUpperCase", "firstUpperCaseInvariant", "titleCase", - "kebabCase" + "kebabCase", + "snakeCase" ] } } @@ -1051,6 +1052,14 @@ "enum": ["kebabCase"] } } + }, + { + "description": "Converts the value to snake case using the casing rules of the invariant culture.", + "properties": { + "identifier": { + "enum": ["snakeCase"] + } + } } ] } diff --git a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ValueFormRegistry.cs b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ValueFormRegistry.cs index 95b9c2e7251..30fd6df8c68 100644 --- a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ValueFormRegistry.cs +++ b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ValueFormRegistry.cs @@ -29,6 +29,7 @@ internal static class ValueFormRegistry new FirstUpperCaseInvariantValueFormFactory(), new FirstLowerCaseInvariantValueFormFactory(), new KebabCaseValueFormFactory(), + new SnakeCaseValueFormFactory(), new TitleCaseValueFormFactory(), }; diff --git a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ValueForms/SnakeCaseValueFormFactory.cs b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ValueForms/SnakeCaseValueFormFactory.cs new file mode 100644 index 00000000000..8fb064b7597 --- /dev/null +++ b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ValueForms/SnakeCaseValueFormFactory.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.RegularExpressions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ValueForms +{ + internal class SnakeCaseValueFormFactory : ActionableValueFormFactory + { + internal const string FormIdentifier = "snakeCase"; + + internal SnakeCaseValueFormFactory() : base(FormIdentifier) { } + + protected override string Process(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return string.Empty; + } + + Regex pattern = new(@"(?:\p{Lu}\p{M}*)?(?:\p{Ll}\p{M}*)+|(?:\p{Lu}\p{M}*)+(?!\p{Ll})|\p{N}+|[^\p{C}\p{P}\p{Z}]+|[\u2700-\u27BF]"); + return string.Join("_", pattern.Matches(value).Cast().Select(m => m.Value)).ToLowerInvariant(); + } + } +} diff --git a/test/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/ValueFormTests/SnakeCaseValueFormTests.cs b/test/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/ValueFormTests/SnakeCaseValueFormTests.cs new file mode 100644 index 00000000000..5fb342aaf1e --- /dev/null +++ b/test/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/ValueFormTests/SnakeCaseValueFormTests.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ValueForms; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.ValueFormTests +{ + public class SnakeCaseValueFormTests + { + [Theory] + [InlineData("I", "i")] + [InlineData("IO", "io")] + [InlineData("FileIO", "file_io")] + [InlineData("SignalR", "signal_r")] + [InlineData("IOStream", "io_stream")] + [InlineData("COMObject", "com_object")] + [InlineData("WebAPI", "web_api")] + [InlineData("XProjectX", "x_project_x")] + [InlineData("NextXXXProject", "next_xxx_project")] + [InlineData("NoNewProject", "no_new_project")] + [InlineData("NONewProject", "no_new_project")] + [InlineData("NewProjectName", "new_project_name")] + [InlineData("ABBREVIATIONAndSomeName", "abbreviation_and_some_name")] + [InlineData("NoNoNoNoNoNoNoName", "no_no_no_no_no_no_no_name")] + [InlineData("AnotherNewNewNewNewNewProjectName", "another_new_new_new_new_new_project_name")] + [InlineData("Param1TestValue", "param_1_test_value")] + [InlineData("Windows10", "windows_10")] + [InlineData("WindowsServer2016R2", "windows_server_2016_r_2")] + [InlineData("", "")] + [InlineData(";MyWord;", "my_word")] + [InlineData("My Word", "my_word")] + [InlineData("My Word", "my_word")] + [InlineData(";;;;;", "")] + [InlineData(" ", "")] + [InlineData("Simple TEXT_here", "simple_text_here")] + [InlineData("НоваяПеременная", "новая_переменная")] + public void SnakeCaseWorksAsExpected(string input, string expected) + { + IValueForm? model = new SnakeCaseValueFormFactory().Create("test"); + string actual = model.Process(input, new Dictionary()); + Assert.Equal(expected, actual); + } + + [Fact] + public void CanHandleNullValue() + { + IValueForm model = new SnakeCaseValueFormFactory().Create("test"); + Assert.Throws(() => model.Process(null!, new Dictionary())); + } + } +} diff --git a/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/.template.config/template.json b/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/.template.config/template.json index 4633a589646..b0fae1d2163 100644 --- a/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/.template.config/template.json +++ b/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/.template.config/template.json @@ -80,6 +80,9 @@ "kebab": { "identifier": "kebabCase" }, + "snake": { + "identifier": "snakeCase" + }, "title": { "identifier": "titleCase" }