Browse Source

Add Options, Functions and Settings to convert Node-Names and Strings to kebab-case

- refactored and renamed String::_camelcase_to_underscore to String:_separate_compound_words
- refactored String::to_snake_case to work with the refactored String::_separate_compound_words
- created char_utils::is_hyphen to catch all hyphen variants in kebab-case conversion
- created String::to_kebab_case using the new String::_separate_compound_words
- created corresponding Documentation in String and StringName
- simplified both switch statements in EditorNode and ProjectDialog
- added new kebab-casing Option for Node Names in ProjectSettings
- added missing camelCase Options to Scene- and Node-Names in ProjectSettings
- simplified Mono RuntimeInterop Functions
- hooked up the ConnectionsDialog
- created additional Unit Tests
Priahoud 1 year ago
parent
commit
bf963e767e

+ 1 - 0
core/object/script_language.cpp

@@ -633,6 +633,7 @@ void ScriptLanguage::_bind_methods() {
 	BIND_ENUM_CONSTANT(SCRIPT_NAME_CASING_PASCAL_CASE);
 	BIND_ENUM_CONSTANT(SCRIPT_NAME_CASING_PASCAL_CASE);
 	BIND_ENUM_CONSTANT(SCRIPT_NAME_CASING_SNAKE_CASE);
 	BIND_ENUM_CONSTANT(SCRIPT_NAME_CASING_SNAKE_CASE);
 	BIND_ENUM_CONSTANT(SCRIPT_NAME_CASING_KEBAB_CASE);
 	BIND_ENUM_CONSTANT(SCRIPT_NAME_CASING_KEBAB_CASE);
+	BIND_ENUM_CONSTANT(SCRIPT_NAME_CASING_CAMEL_CASE);
 }
 }
 
 
 bool PlaceHolderScriptInstance::set(const StringName &p_name, const Variant &p_value) {
 bool PlaceHolderScriptInstance::set(const StringName &p_name, const Variant &p_value) {

+ 1 - 0
core/object/script_language.h

@@ -246,6 +246,7 @@ public:
 		SCRIPT_NAME_CASING_PASCAL_CASE,
 		SCRIPT_NAME_CASING_PASCAL_CASE,
 		SCRIPT_NAME_CASING_SNAKE_CASE,
 		SCRIPT_NAME_CASING_SNAKE_CASE,
 		SCRIPT_NAME_CASING_KEBAB_CASE,
 		SCRIPT_NAME_CASING_KEBAB_CASE,
+		SCRIPT_NAME_CASING_CAMEL_CASE,
 	};
 	};
 
 
 	struct ScriptTemplate {
 	struct ScriptTemplate {

+ 4 - 0
core/string/char_utils.h

@@ -135,3 +135,7 @@ constexpr bool is_punct(char32_t p_char) {
 constexpr bool is_underscore(char32_t p_char) {
 constexpr bool is_underscore(char32_t p_char) {
 	return (p_char == '_');
 	return (p_char == '_');
 }
 }
+
+constexpr bool is_hyphen(char32_t p_char) {
+	return (p_char == '-') || (p_char == 0x2010) || (p_char == 0x2011);
+}

+ 52 - 20
core/string/ustring.cpp

@@ -882,15 +882,15 @@ const char32_t *String::get_data() const {
 	return size() ? &operator[](0) : &zero;
 	return size() ? &operator[](0) : &zero;
 }
 }
 
 
-String String::_camelcase_to_underscore() const {
-	const char32_t *cstr = get_data();
-	String new_string;
-	int start_index = 0;
-
+String String::_separate_compound_words() const {
 	if (length() == 0) {
 	if (length() == 0) {
 		return *this;
 		return *this;
 	}
 	}
 
 
+	const char32_t *cstr = get_data();
+	int start_index = 0;
+	String new_string;
+
 	bool is_prev_upper = is_unicode_upper_case(cstr[0]);
 	bool is_prev_upper = is_unicode_upper_case(cstr[0]);
 	bool is_prev_lower = is_unicode_lower_case(cstr[0]);
 	bool is_prev_lower = is_unicode_lower_case(cstr[0]);
 	bool is_prev_digit = is_digit(cstr[0]);
 	bool is_prev_digit = is_digit(cstr[0]);
@@ -911,7 +911,7 @@ String String::_camelcase_to_underscore() const {
 		const bool cond_d = (is_prev_upper || is_prev_lower) && is_curr_digit; // A2, a2
 		const bool cond_d = (is_prev_upper || is_prev_lower) && is_curr_digit; // A2, a2
 
 
 		if (cond_a || cond_b || cond_c || cond_d) {
 		if (cond_a || cond_b || cond_c || cond_d) {
-			new_string += substr(start_index, i - start_index) + "_";
+			new_string += substr(start_index, i - start_index) + " ";
 			start_index = i;
 			start_index = i;
 		}
 		}
 
 
@@ -921,40 +921,72 @@ String String::_camelcase_to_underscore() const {
 	}
 	}
 
 
 	new_string += substr(start_index, size() - start_index);
 	new_string += substr(start_index, size() - start_index);
+
+	for (int i = 0; i < new_string.size(); i++) {
+		const bool whitespace = is_whitespace(new_string[i]);
+		const bool underscore = is_underscore(new_string[i]);
+		const bool hyphen = is_hyphen(new_string[i]);
+
+		if (whitespace || underscore || hyphen) {
+			new_string[i] = ' ';
+		}
+	}
+
 	return new_string.to_lower();
 	return new_string.to_lower();
 }
 }
 
 
 String String::capitalize() const {
 String String::capitalize() const {
-	String aux = _camelcase_to_underscore().replace_char('_', ' ').strip_edges();
-	String cap;
-	for (int i = 0; i < aux.get_slice_count(" "); i++) {
-		String slice = aux.get_slicec(' ', i);
+	String words = _separate_compound_words().strip_edges();
+	String ret;
+	for (int i = 0; i < words.get_slice_count(" "); i++) {
+		String slice = words.get_slicec(' ', i);
 		if (slice.length() > 0) {
 		if (slice.length() > 0) {
 			slice[0] = _find_upper(slice[0]);
 			slice[0] = _find_upper(slice[0]);
 			if (i > 0) {
 			if (i > 0) {
-				cap += " ";
+				ret += " ";
 			}
 			}
-			cap += slice;
+			ret += slice;
 		}
 		}
 	}
 	}
-
-	return cap;
+	return ret;
 }
 }
 
 
 String String::to_camel_case() const {
 String String::to_camel_case() const {
-	String s = to_pascal_case();
-	if (!s.is_empty()) {
-		s[0] = _find_lower(s[0]);
+	String words = _separate_compound_words().strip_edges();
+	String ret;
+	for (int i = 0; i < words.get_slice_count(" "); i++) {
+		String slice = words.get_slicec(' ', i);
+		if (slice.length() > 0) {
+			if (i == 0) {
+				slice[0] = _find_lower(slice[0]);
+			} else {
+				slice[0] = _find_upper(slice[0]);
+			}
+			ret += slice;
+		}
 	}
 	}
-	return s;
+	return ret;
 }
 }
 
 
 String String::to_pascal_case() const {
 String String::to_pascal_case() const {
-	return capitalize().remove_char(' ');
+	String words = _separate_compound_words().strip_edges();
+	String ret;
+	for (int i = 0; i < words.get_slice_count(" "); i++) {
+		String slice = words.get_slicec(' ', i);
+		if (slice.length() > 0) {
+			slice[0] = _find_upper(slice[0]);
+			ret += slice;
+		}
+	}
+	return ret;
 }
 }
 
 
 String String::to_snake_case() const {
 String String::to_snake_case() const {
-	return _camelcase_to_underscore().replace_char(' ', '_').strip_edges();
+	return _separate_compound_words().replace_char(' ', '_');
+}
+
+String String::to_kebab_case() const {
+	return _separate_compound_words().replace_char(' ', '-');
 }
 }
 
 
 String String::get_with_code_lines() const {
 String String::get_with_code_lines() const {

+ 2 - 1
core/string/ustring.h

@@ -279,7 +279,7 @@ class String {
 	bool _base_is_subsequence_of(const String &p_string, bool case_insensitive) const;
 	bool _base_is_subsequence_of(const String &p_string, bool case_insensitive) const;
 	int _count(const String &p_string, int p_from, int p_to, bool p_case_insensitive) const;
 	int _count(const String &p_string, int p_from, int p_to, bool p_case_insensitive) const;
 	int _count(const char *p_string, int p_from, int p_to, bool p_case_insensitive) const;
 	int _count(const char *p_string, int p_from, int p_to, bool p_case_insensitive) const;
-	String _camelcase_to_underscore() const;
+	String _separate_compound_words() const;
 
 
 public:
 public:
 	enum {
 	enum {
@@ -454,6 +454,7 @@ public:
 	String to_camel_case() const;
 	String to_camel_case() const;
 	String to_pascal_case() const;
 	String to_pascal_case() const;
 	String to_snake_case() const;
 	String to_snake_case() const;
+	String to_kebab_case() const;
 
 
 	String get_with_code_lines() const;
 	String get_with_code_lines() const;
 	int get_slice_count(const String &p_splitter) const;
 	int get_slice_count(const String &p_splitter) const;

+ 1 - 0
core/variant/variant_call.cpp

@@ -1751,6 +1751,7 @@ static void _register_variant_builtin_methods_string() {
 	bind_string_method(to_camel_case, sarray(), varray());
 	bind_string_method(to_camel_case, sarray(), varray());
 	bind_string_method(to_pascal_case, sarray(), varray());
 	bind_string_method(to_pascal_case, sarray(), varray());
 	bind_string_method(to_snake_case, sarray(), varray());
 	bind_string_method(to_snake_case, sarray(), varray());
+	bind_string_method(to_kebab_case, sarray(), varray());
 	bind_string_methodv(split, static_cast<Vector<String> (String::*)(const String &, bool, int) const>(&String::split), sarray("delimiter", "allow_empty", "maxsplit"), varray("", true, 0));
 	bind_string_methodv(split, static_cast<Vector<String> (String::*)(const String &, bool, int) const>(&String::split), sarray("delimiter", "allow_empty", "maxsplit"), varray("", true, 0));
 	bind_string_methodv(rsplit, static_cast<Vector<String> (String::*)(const String &, bool, int) const>(&String::rsplit), sarray("delimiter", "allow_empty", "maxsplit"), varray("", true, 0));
 	bind_string_methodv(rsplit, static_cast<Vector<String> (String::*)(const String &, bool, int) const>(&String::rsplit), sarray("delimiter", "allow_empty", "maxsplit"), varray("", true, 0));
 	bind_string_method(split_floats, sarray("delimiter", "allow_empty"), varray(true));
 	bind_string_method(split_floats, sarray("delimiter", "allow_empty"), varray(true));

+ 2 - 0
doc/classes/ScriptLanguage.xml

@@ -15,5 +15,7 @@
 		</constant>
 		</constant>
 		<constant name="SCRIPT_NAME_CASING_KEBAB_CASE" value="3" enum="ScriptNameCasing">
 		<constant name="SCRIPT_NAME_CASING_KEBAB_CASE" value="3" enum="ScriptNameCasing">
 		</constant>
 		</constant>
+		<constant name="SCRIPT_NAME_CASING_CAMEL_CASE" value="4" enum="ScriptNameCasing">
+		</constant>
 	</constants>
 	</constants>
 </class>
 </class>

+ 19 - 0
doc/classes/String.xml

@@ -1040,6 +1040,25 @@
 				[/codeblock]
 				[/codeblock]
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="to_kebab_case" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns the string converted to [code]kebab-case[/code].
+				[b]Note:[/b] Numbers followed by a [i]single[/i] letter are not separated in the conversion to keep some words (such as "2D") together.
+				[codeblocks]
+				[gdscript]
+				"Node2D".to_kebab_case()               # Returns "node-2d"
+				"2nd place".to_kebab_case()            # Returns "2-nd-place"
+				"Texture3DAssetFolder".to_kebab_case() # Returns "texture-3d-asset-folder"
+				[/gdscript]
+				[csharp]
+				"Node2D".ToKebabCase();               // Returns "node-2d"
+				"2nd place".ToKebabCase();            // Returns "2-nd-place"
+				"Texture3DAssetFolder".ToKebabCase(); // Returns "texture-3d-asset-folder"
+				[/csharp]
+				[/codeblocks]
+			</description>
+		</method>
 		<method name="to_lower" qualifiers="const">
 		<method name="to_lower" qualifiers="const">
 			<return type="String" />
 			<return type="String" />
 			<description>
 			<description>

+ 19 - 0
doc/classes/StringName.xml

@@ -948,6 +948,25 @@
 				[/codeblock]
 				[/codeblock]
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="to_kebab_case" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns the string converted to [code]kebab-case[/code].
+				[b]Note:[/b] Numbers followed by a [i]single[/i] letter are not separated in the conversion to keep some words (such as "2D") together.
+				[codeblocks]
+				[gdscript]
+				"Node2D".to_kebab_case()               # Returns "node-2d"
+				"2nd place".to_kebab_case()            # Returns "2-nd-place"
+				"Texture3DAssetFolder".to_kebab_case() # Returns "texture-3d-asset-folder"
+				[/gdscript]
+				[csharp]
+				"Node2D".ToKebabCase();               // Returns "node-2d"
+				"2nd place".ToKebabCase();            // Returns "2-nd-place"
+				"Texture3DAssetFolder".ToKebabCase(); // Returns "texture-3d-asset-folder"
+				[/csharp]
+				[/codeblocks]
+			</description>
+		</method>
 		<method name="to_lower" qualifiers="const">
 		<method name="to_lower" qualifiers="const">
 			<return type="String" />
 			<return type="String" />
 			<description>
 			<description>

+ 2 - 0
editor/connections_dialog.cpp

@@ -255,10 +255,12 @@ StringName ConnectDialog::generate_method_callback_name(Node *p_source, const St
 	subst["NodeName"] = node_name.to_pascal_case();
 	subst["NodeName"] = node_name.to_pascal_case();
 	subst["nodeName"] = node_name.to_camel_case();
 	subst["nodeName"] = node_name.to_camel_case();
 	subst["node_name"] = node_name.to_snake_case();
 	subst["node_name"] = node_name.to_snake_case();
+	subst["node-name"] = node_name.to_kebab_case();
 
 
 	subst["SignalName"] = p_signal_name.to_pascal_case();
 	subst["SignalName"] = p_signal_name.to_pascal_case();
 	subst["signalName"] = p_signal_name.to_camel_case();
 	subst["signalName"] = p_signal_name.to_camel_case();
 	subst["signal_name"] = p_signal_name.to_snake_case();
 	subst["signal_name"] = p_signal_name.to_snake_case();
+	subst["signal-name"] = p_signal_name.to_kebab_case();
 
 
 	String dst_method;
 	String dst_method;
 	if (p_source == p_target) {
 	if (p_source == p_target) {

+ 10 - 6
editor/editor_node.cpp

@@ -3374,11 +3374,13 @@ String EditorNode::adjust_scene_name_casing(const String &p_root_name) {
 			// Use casing of the root node.
 			// Use casing of the root node.
 			break;
 			break;
 		case SCENE_NAME_CASING_PASCAL_CASE:
 		case SCENE_NAME_CASING_PASCAL_CASE:
-			return p_root_name.replace_char('-', '_').to_pascal_case();
+			return p_root_name.to_pascal_case();
 		case SCENE_NAME_CASING_SNAKE_CASE:
 		case SCENE_NAME_CASING_SNAKE_CASE:
-			return p_root_name.replace_char('-', '_').to_snake_case();
+			return p_root_name.to_snake_case();
 		case SCENE_NAME_CASING_KEBAB_CASE:
 		case SCENE_NAME_CASING_KEBAB_CASE:
-			return p_root_name.to_snake_case().replace_char('_', '-');
+			return p_root_name.to_kebab_case();
+		case SCENE_NAME_CASING_CAMEL_CASE:
+			return p_root_name.to_camel_case();
 	}
 	}
 	return p_root_name;
 	return p_root_name;
 }
 }
@@ -3395,11 +3397,13 @@ String EditorNode::adjust_script_name_casing(const String &p_file_name, ScriptLa
 			// Script language has no preference, so do not adjust.
 			// Script language has no preference, so do not adjust.
 			break;
 			break;
 		case ScriptLanguage::SCRIPT_NAME_CASING_PASCAL_CASE:
 		case ScriptLanguage::SCRIPT_NAME_CASING_PASCAL_CASE:
-			return p_file_name.replace_char('-', '_').to_pascal_case();
+			return p_file_name.to_pascal_case();
 		case ScriptLanguage::SCRIPT_NAME_CASING_SNAKE_CASE:
 		case ScriptLanguage::SCRIPT_NAME_CASING_SNAKE_CASE:
-			return p_file_name.replace_char('-', '_').to_snake_case();
+			return p_file_name.to_snake_case();
 		case ScriptLanguage::SCRIPT_NAME_CASING_KEBAB_CASE:
 		case ScriptLanguage::SCRIPT_NAME_CASING_KEBAB_CASE:
-			return p_file_name.to_snake_case().replace_char('_', '-');
+			return p_file_name.to_kebab_case();
+		case ScriptLanguage::SCRIPT_NAME_CASING_CAMEL_CASE:
+			return p_file_name.to_camel_case();
 	}
 	}
 	return p_file_name;
 	return p_file_name;
 }
 }

+ 1 - 0
editor/editor_node.h

@@ -117,6 +117,7 @@ public:
 		SCENE_NAME_CASING_PASCAL_CASE,
 		SCENE_NAME_CASING_PASCAL_CASE,
 		SCENE_NAME_CASING_SNAKE_CASE,
 		SCENE_NAME_CASING_SNAKE_CASE,
 		SCENE_NAME_CASING_KEBAB_CASE,
 		SCENE_NAME_CASING_KEBAB_CASE,
+		SCENE_NAME_CASING_CAMEL_CASE,
 	};
 	};
 
 
 	enum ActionOnPlay {
 	enum ActionOnPlay {

+ 1 - 1
editor/project_manager/project_dialog.cpp

@@ -277,7 +277,7 @@ void ProjectDialog::_update_target_auto_dir() {
 		case 0: // No convention
 		case 0: // No convention
 			break;
 			break;
 		case 1: // kebab-case
 		case 1: // kebab-case
-			new_auto_dir = new_auto_dir.to_lower().replace_char(' ', '-');
+			new_auto_dir = new_auto_dir.to_kebab_case();
 			break;
 			break;
 		case 2: // snake_case
 		case 2: // snake_case
 			new_auto_dir = new_auto_dir.to_snake_case();
 			new_auto_dir = new_auto_dir.to_snake_case();

+ 2 - 2
editor/register_editor_types.cpp

@@ -279,8 +279,8 @@ void register_editor_types() {
 
 
 	GLOBAL_DEF("editor/naming/default_signal_callback_name", "_on_{node_name}_{signal_name}");
 	GLOBAL_DEF("editor/naming/default_signal_callback_name", "_on_{node_name}_{signal_name}");
 	GLOBAL_DEF("editor/naming/default_signal_callback_to_self_name", "_on_{signal_name}");
 	GLOBAL_DEF("editor/naming/default_signal_callback_to_self_name", "_on_{signal_name}");
-	GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/naming/scene_name_casing", PROPERTY_HINT_ENUM, "Auto,PascalCase,snake_case,kebab-case"), EditorNode::SCENE_NAME_CASING_SNAKE_CASE);
-	GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/naming/script_name_casing", PROPERTY_HINT_ENUM, "Auto,PascalCase,snake_case,kebab-case"), ScriptLanguage::SCRIPT_NAME_CASING_AUTO);
+	GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/naming/scene_name_casing", PROPERTY_HINT_ENUM, "Auto,PascalCase,snake_case,kebab-case,camelCase"), EditorNode::SCENE_NAME_CASING_SNAKE_CASE);
+	GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/naming/script_name_casing", PROPERTY_HINT_ENUM, "Auto,PascalCase,snake_case,kebab-case,camelCase"), ScriptLanguage::SCRIPT_NAME_CASING_AUTO);
 
 
 	GLOBAL_DEF("editor/import/reimport_missing_imported_files", true);
 	GLOBAL_DEF("editor/import/reimport_missing_imported_files", true);
 	GLOBAL_DEF("editor/import/use_multiple_threads", true);
 	GLOBAL_DEF("editor/import/use_multiple_threads", true);

+ 6 - 0
modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs

@@ -503,6 +503,9 @@ namespace Godot.NativeInterop
         public static partial void godotsharp_string_simplify_path(scoped in godot_string p_self,
         public static partial void godotsharp_string_simplify_path(scoped in godot_string p_self,
             out godot_string r_simplified_path);
             out godot_string r_simplified_path);
 
 
+        public static partial void godotsharp_string_capitalize(scoped in godot_string p_self,
+            out godot_string r_capitalized);
+
         public static partial void godotsharp_string_to_camel_case(scoped in godot_string p_self,
         public static partial void godotsharp_string_to_camel_case(scoped in godot_string p_self,
             out godot_string r_camel_case);
             out godot_string r_camel_case);
 
 
@@ -512,6 +515,9 @@ namespace Godot.NativeInterop
         public static partial void godotsharp_string_to_snake_case(scoped in godot_string p_self,
         public static partial void godotsharp_string_to_snake_case(scoped in godot_string p_self,
             out godot_string r_snake_case);
             out godot_string r_snake_case);
 
 
+        public static partial void godotsharp_string_to_kebab_case(scoped in godot_string p_self,
+            out godot_string r_kebab_case);
+
         // NodePath
         // NodePath
 
 
         public static partial void godotsharp_node_path_get_as_property_path(in godot_node_path p_self,
         public static partial void godotsharp_node_path_get_as_property_path(in godot_node_path p_self,

+ 14 - 58
modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs

@@ -314,22 +314,10 @@ namespace Godot
         /// <returns>The capitalized string.</returns>
         /// <returns>The capitalized string.</returns>
         public static string Capitalize(this string instance)
         public static string Capitalize(this string instance)
         {
         {
-            string aux = instance.CamelcaseToUnderscore(true).Replace("_", " ", StringComparison.Ordinal).Trim();
-            string cap = string.Empty;
-
-            for (int i = 0; i < aux.GetSliceCount(" "); i++)
-            {
-                string slice = aux.GetSliceCharacter(' ', i);
-                if (slice.Length > 0)
-                {
-                    slice = char.ToUpperInvariant(slice[0]) + slice.Substring(1);
-                    if (i > 0)
-                        cap += " ";
-                    cap += slice;
-                }
-            }
-
-            return cap;
+            using godot_string instanceStr = Marshaling.ConvertStringToNative(instance);
+            NativeFuncs.godotsharp_string_capitalize(instanceStr, out godot_string capitalized);
+            using (capitalized)
+                return Marshaling.ConvertStringToManaged(capitalized);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -371,49 +359,17 @@ namespace Godot
                 return Marshaling.ConvertStringToManaged(snakeCase);
                 return Marshaling.ConvertStringToManaged(snakeCase);
         }
         }
 
 
-        private static string CamelcaseToUnderscore(this string instance, bool lowerCase)
+        /// <summary>
+        /// Returns the string converted to <c>kebab-case</c>.
+        /// </summary>
+        /// <param name="instance">The string to convert.</param>
+        /// <returns>The converted string.</returns>
+        public static string ToKebabCase(this string instance)
         {
         {
-            string newString = string.Empty;
-            int startIndex = 0;
-
-            for (int i = 1; i < instance.Length; i++)
-            {
-                bool isUpper = char.IsUpper(instance[i]);
-                bool isNumber = char.IsDigit(instance[i]);
-
-                bool areNext2Lower = false;
-                bool isNextLower = false;
-                bool isNextNumber = false;
-                bool wasPrecedentUpper = char.IsUpper(instance[i - 1]);
-                bool wasPrecedentNumber = char.IsDigit(instance[i - 1]);
-
-                if (i + 2 < instance.Length)
-                {
-                    areNext2Lower = char.IsLower(instance[i + 1]) && char.IsLower(instance[i + 2]);
-                }
-
-                if (i + 1 < instance.Length)
-                {
-                    isNextLower = char.IsLower(instance[i + 1]);
-                    isNextNumber = char.IsDigit(instance[i + 1]);
-                }
-
-                bool condA = isUpper && !wasPrecedentUpper && !wasPrecedentNumber;
-                bool condB = wasPrecedentUpper && isUpper && areNext2Lower;
-                bool condC = isNumber && !wasPrecedentNumber;
-                bool canBreakNumberLetter = isNumber && !wasPrecedentNumber && isNextLower;
-                bool canBreakLetterNumber = !isNumber && wasPrecedentNumber && (isNextLower || isNextNumber);
-
-                bool shouldSplit = condA || condB || condC || canBreakNumberLetter || canBreakLetterNumber;
-                if (shouldSplit)
-                {
-                    newString += string.Concat(instance.AsSpan(startIndex, i - startIndex), "_");
-                    startIndex = i;
-                }
-            }
-
-            newString += instance.Substring(startIndex, instance.Length - startIndex);
-            return lowerCase ? newString.ToLowerInvariant() : newString;
+            using godot_string instanceStr = Marshaling.ConvertStringToNative(instance);
+            NativeFuncs.godotsharp_string_to_kebab_case(instanceStr, out godot_string kebabCase);
+            using (kebabCase)
+                return Marshaling.ConvertStringToManaged(kebabCase);
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 10 - 0
modules/mono/glue/runtime_interop.cpp

@@ -1289,6 +1289,10 @@ void godotsharp_string_simplify_path(const String *p_self, String *r_simplified_
 	memnew_placement(r_simplified_path, String(p_self->simplify_path()));
 	memnew_placement(r_simplified_path, String(p_self->simplify_path()));
 }
 }
 
 
+void godotsharp_string_capitalize(const String *p_self, String *r_capitalized) {
+	memnew_placement(r_capitalized, String(p_self->capitalize()));
+}
+
 void godotsharp_string_to_camel_case(const String *p_self, String *r_camel_case) {
 void godotsharp_string_to_camel_case(const String *p_self, String *r_camel_case) {
 	memnew_placement(r_camel_case, String(p_self->to_camel_case()));
 	memnew_placement(r_camel_case, String(p_self->to_camel_case()));
 }
 }
@@ -1301,6 +1305,10 @@ void godotsharp_string_to_snake_case(const String *p_self, String *r_snake_case)
 	memnew_placement(r_snake_case, String(p_self->to_snake_case()));
 	memnew_placement(r_snake_case, String(p_self->to_snake_case()));
 }
 }
 
 
+void godotsharp_string_to_kebab_case(const String *p_self, String *r_kebab_case) {
+	memnew_placement(r_kebab_case, String(p_self->to_kebab_case()));
+}
+
 void godotsharp_node_path_get_as_property_path(const NodePath *p_ptr, NodePath *r_dest) {
 void godotsharp_node_path_get_as_property_path(const NodePath *p_ptr, NodePath *r_dest) {
 	memnew_placement(r_dest, NodePath(p_ptr->get_as_property_path()));
 	memnew_placement(r_dest, NodePath(p_ptr->get_as_property_path()));
 }
 }
@@ -1700,9 +1708,11 @@ static const void *unmanaged_callbacks[]{
 	(void *)godotsharp_dictionary_get_typed_value_script,
 	(void *)godotsharp_dictionary_get_typed_value_script,
 	(void *)godotsharp_dictionary_to_string,
 	(void *)godotsharp_dictionary_to_string,
 	(void *)godotsharp_string_simplify_path,
 	(void *)godotsharp_string_simplify_path,
+	(void *)godotsharp_string_capitalize,
 	(void *)godotsharp_string_to_camel_case,
 	(void *)godotsharp_string_to_camel_case,
 	(void *)godotsharp_string_to_pascal_case,
 	(void *)godotsharp_string_to_pascal_case,
 	(void *)godotsharp_string_to_snake_case,
 	(void *)godotsharp_string_to_snake_case,
+	(void *)godotsharp_string_to_kebab_case,
 	(void *)godotsharp_node_path_get_as_property_path,
 	(void *)godotsharp_node_path_get_as_property_path,
 	(void *)godotsharp_node_path_get_concatenated_names,
 	(void *)godotsharp_node_path_get_concatenated_names,
 	(void *)godotsharp_node_path_get_concatenated_subnames,
 	(void *)godotsharp_node_path_get_concatenated_subnames,

+ 3 - 1
scene/main/node.cpp

@@ -1641,6 +1641,8 @@ String Node::adjust_name_casing(const String &p_name) {
 			return p_name.to_camel_case();
 			return p_name.to_camel_case();
 		case NAME_CASING_SNAKE_CASE:
 		case NAME_CASING_SNAKE_CASE:
 			return p_name.to_snake_case();
 			return p_name.to_snake_case();
+		case NAME_CASING_KEBAB_CASE:
+			return p_name.to_kebab_case();
 	}
 	}
 	return p_name;
 	return p_name;
 }
 }
@@ -3830,7 +3832,7 @@ RID Node::get_accessibility_element() const {
 
 
 void Node::_bind_methods() {
 void Node::_bind_methods() {
 	GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/naming/node_name_num_separator", PROPERTY_HINT_ENUM, "None,Space,Underscore,Dash"), 0);
 	GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/naming/node_name_num_separator", PROPERTY_HINT_ENUM, "None,Space,Underscore,Dash"), 0);
-	GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/naming/node_name_casing", PROPERTY_HINT_ENUM, "PascalCase,camelCase,snake_case"), NAME_CASING_PASCAL_CASE);
+	GLOBAL_DEF(PropertyInfo(Variant::INT, "editor/naming/node_name_casing", PROPERTY_HINT_ENUM, "PascalCase,camelCase,snake_case,kebab-case"), NAME_CASING_PASCAL_CASE);
 
 
 	ClassDB::bind_static_method("Node", D_METHOD("print_orphan_nodes"), &Node::print_orphan_nodes);
 	ClassDB::bind_static_method("Node", D_METHOD("print_orphan_nodes"), &Node::print_orphan_nodes);
 	ClassDB::bind_method(D_METHOD("add_sibling", "sibling", "force_readable_name"), &Node::add_sibling, DEFVAL(false));
 	ClassDB::bind_method(D_METHOD("add_sibling", "sibling", "force_readable_name"), &Node::add_sibling, DEFVAL(false));

+ 2 - 1
scene/main/node.h

@@ -106,7 +106,8 @@ public:
 	enum NameCasing {
 	enum NameCasing {
 		NAME_CASING_PASCAL_CASE,
 		NAME_CASING_PASCAL_CASE,
 		NAME_CASING_CAMEL_CASE,
 		NAME_CASING_CAMEL_CASE,
-		NAME_CASING_SNAKE_CASE
+		NAME_CASING_SNAKE_CASE,
+		NAME_CASING_KEBAB_CASE,
 	};
 	};
 
 
 	enum InternalMode {
 	enum InternalMode {

+ 43 - 24
tests/core/string/test_string.h

@@ -1451,6 +1451,14 @@ TEST_CASE("[String] Capitalize against many strings") {
 	output = "Snake Snake Case";
 	output = "Snake Snake Case";
 	CHECK(input.capitalize() == output);
 	CHECK(input.capitalize() == output);
 
 
+	input = "kebab-case";
+	output = "Kebab Case";
+	CHECK(input.capitalize() == output);
+
+	input = "kebab-kebab-case";
+	output = "Kebab Kebab Case";
+	CHECK(input.capitalize() == output);
+
 	input = "sha256sum";
 	input = "sha256sum";
 	output = "Sha 256 Sum";
 	output = "Sha 256 Sum";
 	CHECK(input.capitalize() == output);
 	CHECK(input.capitalize() == output);
@@ -1471,6 +1479,14 @@ TEST_CASE("[String] Capitalize against many strings") {
 	output = "Snake Case Function( Snake Case Arg )";
 	output = "Snake Case Function( Snake Case Arg )";
 	CHECK(input.capitalize() == output);
 	CHECK(input.capitalize() == output);
 
 
+	input = "kebab-case-function( kebab-case-arg )";
+	output = "Kebab Case Function( Kebab Case Arg )";
+	CHECK(input.capitalize() == output);
+
+	input = "kebab_case_function( kebab_case_arg )";
+	output = "Kebab Case Function( Kebab Case Arg )";
+	CHECK(input.capitalize() == output);
+
 	input = U"словоСлово_слово слово";
 	input = U"словоСлово_слово слово";
 	output = U"Слово Слово Слово Слово";
 	output = U"Слово Слово Слово Слово";
 	CHECK(input.capitalize() == output);
 	CHECK(input.capitalize() == output);
@@ -1489,35 +1505,37 @@ struct StringCasesTestCase {
 	const char32_t *camel_case;
 	const char32_t *camel_case;
 	const char32_t *pascal_case;
 	const char32_t *pascal_case;
 	const char32_t *snake_case;
 	const char32_t *snake_case;
+	const char32_t *kebab_case;
 };
 };
 
 
 TEST_CASE("[String] Checking case conversion methods") {
 TEST_CASE("[String] Checking case conversion methods") {
 	StringCasesTestCase test_cases[] = {
 	StringCasesTestCase test_cases[] = {
 		/* clang-format off */
 		/* clang-format off */
-		{ U"2D",                     U"2d",                   U"2d",                   U"2d"                      },
-		{ U"2d",                     U"2d",                   U"2d",                   U"2d"                      },
-		{ U"2db",                    U"2Db",                  U"2Db",                  U"2_db"                    },
-		{ U"Vector3",                U"vector3",              U"Vector3",              U"vector_3"                },
-		{ U"sha256",                 U"sha256",               U"Sha256",               U"sha_256"                 },
-		{ U"Node2D",                 U"node2d",               U"Node2d",               U"node_2d"                 },
-		{ U"RichTextLabel",          U"richTextLabel",        U"RichTextLabel",        U"rich_text_label"         },
-		{ U"HTML5",                  U"html5",                U"Html5",                U"html_5"                  },
-		{ U"Node2DPosition",         U"node2dPosition",       U"Node2dPosition",       U"node_2d_position"        },
-		{ U"Number2Digits",          U"number2Digits",        U"Number2Digits",        U"number_2_digits"         },
-		{ U"get_property_list",      U"getPropertyList",      U"GetPropertyList",      U"get_property_list"       },
-		{ U"get_camera_2d",          U"getCamera2d",          U"GetCamera2d",          U"get_camera_2d"           },
-		{ U"_physics_process",       U"physicsProcess",       U"PhysicsProcess",       U"_physics_process"        },
-		{ U"bytes2var",              U"bytes2Var",            U"Bytes2Var",            U"bytes_2_var"             },
-		{ U"linear2db",              U"linear2Db",            U"Linear2Db",            U"linear_2_db"             },
-		{ U"sha256sum",              U"sha256Sum",            U"Sha256Sum",            U"sha_256_sum"             },
-		{ U"camelCase",              U"camelCase",            U"CamelCase",            U"camel_case"              },
-		{ U"PascalCase",             U"pascalCase",           U"PascalCase",           U"pascal_case"             },
-		{ U"snake_case",             U"snakeCase",            U"SnakeCase",            U"snake_case"              },
-		{ U"Test TEST test",         U"testTestTest",         U"TestTestTest",         U"test_test_test"          },
-		{ U"словоСлово_слово слово", U"словоСловоСловоСлово", U"СловоСловоСловоСлово", U"слово_слово_слово_слово" },
-		{ U"λέξηΛέξη_λέξη λέξη",     U"λέξηΛέξηΛέξηΛέξη",     U"ΛέξηΛέξηΛέξηΛέξη",     U"λέξη_λέξη_λέξη_λέξη"     },
-		{ U"բառԲառ_բառ բառ",         U"բառԲառԲառԲառ",         U"ԲառԲառԲառԲառ",         U"բառ_բառ_բառ_բառ"         },
-		{ nullptr,                   nullptr,                 nullptr,                 nullptr                    },
+		{ U"2D",                     U"2d",                   U"2d",                   U"2d",                      U"2d"                      },
+		{ U"2d",                     U"2d",                   U"2d",                   U"2d",                      U"2d"                      },
+		{ U"2db",                    U"2Db",                  U"2Db",                  U"2_db",                    U"2-db"                    },
+		{ U"Vector3",                U"vector3",              U"Vector3",              U"vector_3",                U"vector-3"                },
+		{ U"sha256",                 U"sha256",               U"Sha256",               U"sha_256",                 U"sha-256"                 },
+		{ U"Node2D",                 U"node2d",               U"Node2d",               U"node_2d",                 U"node-2d"                 },
+		{ U"RichTextLabel",          U"richTextLabel",        U"RichTextLabel",        U"rich_text_label",         U"rich-text-label"         },
+		{ U"HTML5",                  U"html5",                U"Html5",                U"html_5",                  U"html-5"                  },
+		{ U"Node2DPosition",         U"node2dPosition",       U"Node2dPosition",       U"node_2d_position",        U"node-2d-position"        },
+		{ U"Number2Digits",          U"number2Digits",        U"Number2Digits",        U"number_2_digits",         U"number-2-digits"         },
+		{ U"get_property_list",      U"getPropertyList",      U"GetPropertyList",      U"get_property_list",       U"get-property-list"       },
+		{ U"get_camera_2d",          U"getCamera2d",          U"GetCamera2d",          U"get_camera_2d",           U"get-camera-2d"           },
+		{ U"_physics_process",       U"physicsProcess",       U"PhysicsProcess",       U"_physics_process",        U"-physics-process"        },
+		{ U"bytes2var",              U"bytes2Var",            U"Bytes2Var",            U"bytes_2_var",             U"bytes-2-var"             },
+		{ U"linear2db",              U"linear2Db",            U"Linear2Db",            U"linear_2_db",             U"linear-2-db"             },
+		{ U"sha256sum",              U"sha256Sum",            U"Sha256Sum",            U"sha_256_sum",             U"sha-256-sum"             },
+		{ U"camelCase",              U"camelCase",            U"CamelCase",            U"camel_case",              U"camel-case"              },
+		{ U"PascalCase",             U"pascalCase",           U"PascalCase",           U"pascal_case",             U"pascal-case"             },
+		{ U"snake_case",             U"snakeCase",            U"SnakeCase",            U"snake_case",              U"snake-case"              },
+		{ U"kebab-case",             U"kebabCase",            U"KebabCase",            U"kebab_case",              U"kebab-case"              },
+		{ U"Test TEST test",         U"testTestTest",         U"TestTestTest",         U"test_test_test",          U"test-test-test"          },
+		{ U"словоСлово_слово слово", U"словоСловоСловоСлово", U"СловоСловоСловоСлово", U"слово_слово_слово_слово", U"слово-слово-слово-слово" },
+		{ U"λέξηΛέξη_λέξη λέξη",     U"λέξηΛέξηΛέξηΛέξη",     U"ΛέξηΛέξηΛέξηΛέξη",     U"λέξη_λέξη_λέξη_λέξη",     U"λέξη-λέξη-λέξη-λέξη"     },
+		{ U"բառԲառ_բառ բառ",         U"բառԲառԲառԲառ",         U"ԲառԲառԲառԲառ",         U"բառ_բառ_բառ_բառ",         U"բառ-բառ-բառ-բառ"         },
+		{ nullptr,                   nullptr,                 nullptr,                 nullptr,                    nullptr                    },
 		/* clang-format on */
 		/* clang-format on */
 	};
 	};
 
 
@@ -1527,6 +1545,7 @@ TEST_CASE("[String] Checking case conversion methods") {
 		CHECK(input.to_camel_case() == test_cases[idx].camel_case);
 		CHECK(input.to_camel_case() == test_cases[idx].camel_case);
 		CHECK(input.to_pascal_case() == test_cases[idx].pascal_case);
 		CHECK(input.to_pascal_case() == test_cases[idx].pascal_case);
 		CHECK(input.to_snake_case() == test_cases[idx].snake_case);
 		CHECK(input.to_snake_case() == test_cases[idx].snake_case);
+		CHECK(input.to_kebab_case() == test_cases[idx].kebab_case);
 		idx++;
 		idx++;
 	}
 	}
 }
 }