diff --git a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/CreateNewOnMetadataUpdateAttributePass.cs b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/CreateNewOnMetadataUpdateAttributePass.cs index cfac39e1429322c231b883ede0304eafd2bd92ff..9c81968d164b57e290c5a4902684cace1b523308 100644 --- a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/CreateNewOnMetadataUpdateAttributePass.cs +++ b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/CreateNewOnMetadataUpdateAttributePass.cs @@ -16,9 +16,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions { protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) { - if (FileKinds.IsComponent(codeDocument.GetFileKind())) + if (documentNode.DocumentKind != RazorPageDocumentClassifierPass.RazorPageDocumentKind && + documentNode.DocumentKind != MvcViewDocumentClassifierPass.MvcViewDocumentKind) { - // Hot reload does not apply to components. + // Not a MVC file. Skip. return; } diff --git a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/ModelExpressionPass.cs b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/ModelExpressionPass.cs index ff4490563a0e105deaca6c271bf35eb20daa8ba1..6bbceb73f878223cebb67ef9ef43caa12aee6e85 100644 --- a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/ModelExpressionPass.cs +++ b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/ModelExpressionPass.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -15,6 +15,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) { + if (documentNode.DocumentKind != RazorPageDocumentClassifierPass.RazorPageDocumentKind && + documentNode.DocumentKind != MvcViewDocumentClassifierPass.MvcViewDocumentKind) + { + // Not a MVC file. Skip. + return; + } + var visitor = new Visitor(); visitor.Visit(documentNode); } diff --git a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/ViewComponentTagHelperPass.cs b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/ViewComponentTagHelperPass.cs index cc0b873e060a0b3cb2478b840144f95b43f6b5c6..e9b2191d44fef2952f67058383ae46d98a4a4b0c 100644 --- a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/ViewComponentTagHelperPass.cs +++ b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/ViewComponentTagHelperPass.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; @@ -15,6 +15,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) { + if (documentNode.DocumentKind != RazorPageDocumentClassifierPass.RazorPageDocumentKind && + documentNode.DocumentKind != MvcViewDocumentClassifierPass.MvcViewDocumentKind) + { + // Not a MVC file. Skip. + return; + } + var @namespace = documentNode.FindPrimaryNamespace(); var @class = documentNode.FindPrimaryClass(); if (@namespace == null || @class == null) diff --git a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/ModelExpressionPassTest.cs b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/ModelExpressionPassTest.cs index 3465e29ef5916395ca271dc17714558b3b6f3458..9f79d631a57353d21dc1ec3a7c4d813570bb291c 100644 --- a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/ModelExpressionPassTest.cs +++ b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/ModelExpressionPassTest.cs @@ -170,7 +170,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions } } - return codeDocument.GetDocumentIntermediateNode(); + var irNode = codeDocument.GetDocumentIntermediateNode(); + irNode.DocumentKind = MvcViewDocumentClassifierPass.MvcViewDocumentKind; + + return irNode; } private TagHelperIntermediateNode FindTagHelperNode(IntermediateNode node) @@ -205,4 +208,4 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions } } } -} \ No newline at end of file +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultAllowedChildTagDescriptorBuilder.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultAllowedChildTagDescriptorBuilder.cs index eeb5de2442182644e93a404fc3fa56e285f54c59..595f782e52064f538ad6d36828332f689f36ab1a 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultAllowedChildTagDescriptorBuilder.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultAllowedChildTagDescriptorBuilder.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -36,10 +36,10 @@ namespace Microsoft.AspNetCore.Razor.Language public AllowedChildTagDescriptor Build() { - var validationDiagnostics = Validate(); - var diagnostics = new HashSet<RazorDiagnostic>(validationDiagnostics); + var diagnostics = Validate(); if (_diagnostics != null) { + diagnostics ??= new(); diagnostics.UnionWith(_diagnostics); } @@ -47,31 +47,35 @@ namespace Microsoft.AspNetCore.Razor.Language var descriptor = new DefaultAllowedChildTagDescriptor( Name, displayName, - diagnostics.ToArray()); + diagnostics?.ToArray() ?? Array.Empty<RazorDiagnostic>()); return descriptor; } - private IEnumerable<RazorDiagnostic> Validate() + private HashSet<RazorDiagnostic> Validate() { + HashSet<RazorDiagnostic> diagnostics = null; if (string.IsNullOrWhiteSpace(Name)) { var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidRestrictedChildNullOrWhitespace(_parent.GetDisplayName()); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } else if (Name != TagHelperMatchingConventions.ElementCatchAllName) { foreach (var character in Name) { - if (char.IsWhiteSpace(character) || HtmlConventions.InvalidNonWhitespaceHtmlCharacters.Contains(character)) + if (char.IsWhiteSpace(character) || HtmlConventions.IsInvalidNonWhitespaceHtmlCharacters(character)) { var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidRestrictedChild(_parent.GetDisplayName(), Name, character); - - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } } } + + return diagnostics; } } } diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultBoundAttributeDescriptorBuilder.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultBoundAttributeDescriptorBuilder.cs index 8bae2309d7a3328c8bfd8251d508e51698ce0b13..a9b9eb456e22baa56636b6dd3e7ef4d10767569b 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultBoundAttributeDescriptorBuilder.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultBoundAttributeDescriptorBuilder.cs @@ -92,10 +92,10 @@ namespace Microsoft.AspNetCore.Razor.Language public BoundAttributeDescriptor Build() { - var validationDiagnostics = Validate(); - var diagnostics = new HashSet<RazorDiagnostic>(validationDiagnostics); + var diagnostics = Validate(); if (_diagnostics != null) { + diagnostics ??= new(); diagnostics.UnionWith(_diagnostics); } @@ -125,7 +125,7 @@ namespace Microsoft.AspNetCore.Razor.Language CaseSensitive, parameters, new Dictionary<string, string>(Metadata), - diagnostics.ToArray()) + diagnostics?.ToArray() ?? Array.Empty<RazorDiagnostic>()) { IsEditorRequired = IsEditorRequired, }; @@ -159,13 +159,15 @@ namespace Microsoft.AspNetCore.Razor.Language return Name; } - private IEnumerable<RazorDiagnostic> Validate() + private HashSet<RazorDiagnostic> Validate() { // data-* attributes are explicitly not implemented by user agents and are not intended for use on // the server; therefore it's invalid for TagHelpers to bind to them. const string DataDashPrefix = "data-"; var isDirectiveAttribute = this.IsDirectiveAttribute(); + HashSet<RazorDiagnostic> diagnostics = null; + if (string.IsNullOrWhiteSpace(Name)) { if (IndexerAttributeNamePrefix == null) @@ -174,7 +176,8 @@ namespace Microsoft.AspNetCore.Razor.Language _parent.GetDisplayName(), GetDisplayName()); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } } else @@ -186,7 +189,8 @@ namespace Microsoft.AspNetCore.Razor.Language GetDisplayName(), Name); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } StringSegment name = Name; @@ -201,13 +205,14 @@ namespace Microsoft.AspNetCore.Razor.Language GetDisplayName(), Name); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } for (var i = 0; i < name.Length; i++) { var character = name[i]; - if (char.IsWhiteSpace(character) || HtmlConventions.InvalidNonWhitespaceHtmlCharacters.Contains(character)) + if (char.IsWhiteSpace(character) || HtmlConventions.IsInvalidNonWhitespaceHtmlCharacters(character)) { var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidBoundAttributeName( _parent.GetDisplayName(), @@ -215,7 +220,8 @@ namespace Microsoft.AspNetCore.Razor.Language name.Value, character); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } } } @@ -229,7 +235,8 @@ namespace Microsoft.AspNetCore.Razor.Language GetDisplayName(), IndexerAttributeNamePrefix); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } else if (IndexerAttributeNamePrefix.Length > 0 && string.IsNullOrWhiteSpace(IndexerAttributeNamePrefix)) { @@ -237,7 +244,8 @@ namespace Microsoft.AspNetCore.Razor.Language _parent.GetDisplayName(), GetDisplayName()); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } else { @@ -253,13 +261,14 @@ namespace Microsoft.AspNetCore.Razor.Language GetDisplayName(), indexerPrefix.Value); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } for (var i = 0; i < indexerPrefix.Length; i++) { var character = indexerPrefix[i]; - if (char.IsWhiteSpace(character) || HtmlConventions.InvalidNonWhitespaceHtmlCharacters.Contains(character)) + if (char.IsWhiteSpace(character) || HtmlConventions.IsInvalidNonWhitespaceHtmlCharacters(character)) { var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidBoundAttributePrefix( _parent.GetDisplayName(), @@ -267,11 +276,14 @@ namespace Microsoft.AspNetCore.Razor.Language indexerPrefix.Value, character); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } } } } + + return diagnostics; } private void EnsureAttributeParameterBuilders() diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultBoundAttributeParameterDescriptorBuilder.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultBoundAttributeParameterDescriptorBuilder.cs index c93115fe45364d67448af831034cf09c5e21337b..82670124298bb785627e09f8bf2a936a1c6be16f 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultBoundAttributeParameterDescriptorBuilder.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultBoundAttributeParameterDescriptorBuilder.cs @@ -1,6 +1,7 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Linq; @@ -51,10 +52,10 @@ namespace Microsoft.AspNetCore.Razor.Language public BoundAttributeParameterDescriptor Build() { - var validationDiagnostics = Validate(); - var diagnostics = new HashSet<RazorDiagnostic>(validationDiagnostics); + var diagnostics = Validate(); if (_diagnostics != null) { + diagnostics ??= new(); diagnostics.UnionWith(_diagnostics); } var descriptor = new DefaultBoundAttributeParameterDescriptor( @@ -66,7 +67,7 @@ namespace Microsoft.AspNetCore.Razor.Language GetDisplayName(), CaseSensitive, new Dictionary<string, string>(Metadata), - diagnostics.ToArray()); + diagnostics?.ToArray() ?? Array.Empty<RazorDiagnostic>()); return descriptor; } @@ -81,28 +82,34 @@ namespace Microsoft.AspNetCore.Razor.Language return $":{Name}"; } - private IEnumerable<RazorDiagnostic> Validate() + private HashSet<RazorDiagnostic> Validate() { + HashSet<RazorDiagnostic> diagnostics = null; if (string.IsNullOrWhiteSpace(Name)) { + var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidBoundAttributeParameterNullOrWhitespace(_parent.Name); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } else { foreach (var character in Name) { - if (char.IsWhiteSpace(character) || HtmlConventions.InvalidNonWhitespaceHtmlCharacters.Contains(character)) + if (char.IsWhiteSpace(character) || HtmlConventions.IsInvalidNonWhitespaceHtmlCharacters(character)) { var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidBoundAttributeParameterName( _parent.Name, Name, character); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } } } + + return diagnostics; } } } diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRequiredAttributeDescriptorBuilder.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRequiredAttributeDescriptorBuilder.cs index 9e1b98d10767fbfe5a7af00707d1043b161087d1..93f714d5ef08da04ddcc528cf58896e8e5772be2 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRequiredAttributeDescriptorBuilder.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRequiredAttributeDescriptorBuilder.cs @@ -45,10 +45,10 @@ namespace Microsoft.AspNetCore.Razor.Language public RequiredAttributeDescriptor Build() { - var validationDiagnostics = Validate(); - var diagnostics = new HashSet<RazorDiagnostic>(validationDiagnostics); + var diagnostics = Validate(); if (_diagnostics != null) { + diagnostics ??= new(); diagnostics.UnionWith(_diagnostics); } @@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Razor.Language Value, ValueComparisonMode, displayName, - diagnostics.ToArray(), + diagnostics?.ToArray() ?? Array.Empty<RazorDiagnostic>(), new Dictionary<string, string>(Metadata)); return rule; @@ -71,39 +71,47 @@ namespace Microsoft.AspNetCore.Razor.Language return NameComparisonMode == RequiredAttributeDescriptor.NameComparisonMode.PrefixMatch ? string.Concat(Name, "...") : Name; } - private IEnumerable<RazorDiagnostic> Validate() + private HashSet<RazorDiagnostic> Validate() { + HashSet<RazorDiagnostic> diagnostics = null; + if (string.IsNullOrWhiteSpace(Name)) { var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidTargetedAttributeNameNullOrWhitespace(); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } else { - var name = Name; + var name = new StringSegment(Name); var isDirectiveAttribute = this.IsDirectiveAttribute(); if (isDirectiveAttribute && name.StartsWith("@", StringComparison.Ordinal)) { - name = name.Substring(1); + name = name.Subsegment(1); } else if (isDirectiveAttribute) { var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidRequiredDirectiveAttributeName(GetDisplayName(), Name); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } - foreach (var character in name) + for (var i = 0; i < name.Length; i++) { - if (char.IsWhiteSpace(character) || HtmlConventions.InvalidNonWhitespaceHtmlCharacters.Contains(character)) + var character = name[i]; + if (char.IsWhiteSpace(character) || HtmlConventions.IsInvalidNonWhitespaceHtmlCharacters(character)) { var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidTargetedAttributeName(Name, character); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } } } + + return diagnostics; } } } diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultTagMatchingRuleDescriptorBuilder.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultTagMatchingRuleDescriptorBuilder.cs index f17769422e18a234753ade4a75f1adb50c7ca629..944ecc800a4593055bf8150f01d1276240c0a69f 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultTagMatchingRuleDescriptorBuilder.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultTagMatchingRuleDescriptorBuilder.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -65,10 +65,10 @@ namespace Microsoft.AspNetCore.Razor.Language public TagMatchingRuleDescriptor Build() { - var validationDiagnostics = Validate(); - var diagnostics = new HashSet<RazorDiagnostic>(validationDiagnostics); + var diagnostics = Validate(); if (_diagnostics != null) { + diagnostics ??= new(); diagnostics.UnionWith(_diagnostics); } @@ -90,28 +90,32 @@ namespace Microsoft.AspNetCore.Razor.Language TagStructure, CaseSensitive, requiredAttributes, - diagnostics.ToArray()); + diagnostics?.ToArray() ?? Array.Empty<RazorDiagnostic>()); return rule; } - private IEnumerable<RazorDiagnostic> Validate() + private HashSet<RazorDiagnostic> Validate() { + HashSet<RazorDiagnostic> diagnostics = null; + if (string.IsNullOrWhiteSpace(TagName)) { var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidTargetedTagNameNullOrWhitespace(); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } else if (TagName != TagHelperMatchingConventions.ElementCatchAllName) { foreach (var character in TagName) { - if (char.IsWhiteSpace(character) || HtmlConventions.InvalidNonWhitespaceHtmlCharacters.Contains(character)) + if (char.IsWhiteSpace(character) || HtmlConventions.IsInvalidNonWhitespaceHtmlCharacters(character)) { var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidTargetedTagName(TagName, character); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } } } @@ -122,21 +126,25 @@ namespace Microsoft.AspNetCore.Razor.Language { var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidTargetedParentTagNameNullOrWhitespace(); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } else { foreach (var character in ParentTag) { - if (char.IsWhiteSpace(character) || HtmlConventions.InvalidNonWhitespaceHtmlCharacters.Contains(character)) + if (char.IsWhiteSpace(character) || HtmlConventions.IsInvalidNonWhitespaceHtmlCharacters(character)) { var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidTargetedParentTagName(ParentTag, character); - yield return diagnostic; + diagnostics ??= new(); + diagnostics.Add(diagnostic); } } } } + + return diagnostics; } private void EnsureRequiredAttributeBuilders() diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/HtmlConventions.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/HtmlConventions.cs index 29e99c2df1c2565b91b068e490f8984508651c33..c73e8fc8e617be5ff50ced7ee154a8fd5824cee2 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/HtmlConventions.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/HtmlConventions.cs @@ -1,8 +1,7 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.Text.RegularExpressions; namespace Microsoft.AspNetCore.Razor.Language @@ -10,6 +9,8 @@ namespace Microsoft.AspNetCore.Razor.Language public static class HtmlConventions { private const string HtmlCaseRegexReplacement = "-$1$2"; + private static readonly char[] InvalidNonWhitespaceHtmlCharacters = + new[] { '@', '!', '<', '/', '?', '[', '>', ']', '=', '"', '\'', '*' }; // This matches the following AFTER the start of the input string (MATCH). // Any letter/number followed by an uppercase letter then lowercase letter: 1(Aa), a(Aa), A(Aa) @@ -21,8 +22,19 @@ namespace Microsoft.AspNetCore.Razor.Language RegexOptions.None, TimeSpan.FromMilliseconds(500)); - internal static IReadOnlyCollection<char> InvalidNonWhitespaceHtmlCharacters { get; } = new List<char>( - new[] { '@', '!', '<', '/', '?', '[', '>', ']', '=', '"', '\'', '*' }); + + internal static bool IsInvalidNonWhitespaceHtmlCharacters(char testChar) + { + foreach (var c in InvalidNonWhitespaceHtmlCharacters) + { + if (c == testChar) + { + return true; + } + } + + return false; + } /// <summary> /// Converts from pascal/camel case to lower kebab-case.