From d4555ef5fbf706c43faccf5fdb3f023696197727 Mon Sep 17 00:00:00 2001 From: Danil Alexeev Date: Tue, 30 Aug 2022 12:36:24 +0300 Subject: Add `String.to_{camel,pascal,snake}_case` methods --- core/string/ustring.cpp | 85 +++++++++++---------- core/string/ustring.h | 5 +- core/variant/variant_call.cpp | 3 + doc/classes/String.xml | 18 +++++ editor/connections_dialog.cpp | 22 ++---- editor/editor_autoload_settings.cpp | 6 +- editor/editor_node.cpp | 8 +- editor/plugins/bone_map_editor_plugin.cpp | 2 +- editor/plugins/script_text_editor.cpp | 2 +- editor/rename_dialog.cpp | 4 +- editor/script_create_dialog.cpp | 2 +- editor/shader_create_dialog.cpp | 2 +- editor/translations/extract.py | 2 +- .../GodotSharp/Core/NativeInterop/NativeFuncs.cs | 9 +++ .../GodotSharp/GodotSharp/Core/StringExtensions.cs | 39 ++++++++++ modules/mono/glue/runtime_interop.cpp | 15 ++++ scene/main/node.cpp | 11 +-- tests/core/string/test_string.h | 87 +++++++++++++++++++--- 18 files changed, 234 insertions(+), 88 deletions(-) diff --git a/core/string/ustring.cpp b/core/string/ustring.cpp index ed3d4b0b54..d8b93998af 100644 --- a/core/string/ustring.cpp +++ b/core/string/ustring.cpp @@ -970,62 +970,71 @@ const char32_t *String::get_data() const { return size() ? &operator[](0) : &zero; } -String String::capitalize() const { - String aux = this->camelcase_to_underscore(true).replace("_", " ").strip_edges(); - String cap; - for (int i = 0; i < aux.get_slice_count(" "); i++) { - String slice = aux.get_slicec(' ', i); - if (slice.length() > 0) { - slice[0] = _find_upper(slice[0]); - if (i > 0) { - cap += " "; - } - cap += slice; - } - } - - return cap; -} - -String String::camelcase_to_underscore(bool lowercase) const { +String String::_camelcase_to_underscore() const { const char32_t *cstr = get_data(); String new_string; int start_index = 0; for (int i = 1; i < this->size(); i++) { - bool is_upper = is_ascii_upper_case(cstr[i]); - bool is_number = is_digit(cstr[i]); + bool is_prev_upper = is_ascii_upper_case(cstr[i - 1]); + bool is_prev_lower = is_ascii_lower_case(cstr[i - 1]); + bool is_prev_digit = is_digit(cstr[i - 1]); - bool are_next_2_lower = false; - bool is_next_lower = false; - bool is_next_number = false; - bool was_precedent_upper = is_ascii_upper_case(cstr[i - 1]); - bool was_precedent_number = is_digit(cstr[i - 1]); - - if (i + 2 < this->size()) { - are_next_2_lower = is_ascii_lower_case(cstr[i + 1]) && is_ascii_lower_case(cstr[i + 2]); - } + bool is_curr_upper = is_ascii_upper_case(cstr[i]); + bool is_curr_lower = is_ascii_lower_case(cstr[i]); + bool is_curr_digit = is_digit(cstr[i]); + bool is_next_lower = false; if (i + 1 < this->size()) { is_next_lower = is_ascii_lower_case(cstr[i + 1]); - is_next_number = is_digit(cstr[i + 1]); } - const bool cond_a = is_upper && !was_precedent_upper && !was_precedent_number; - const bool cond_b = was_precedent_upper && is_upper && are_next_2_lower; - const bool cond_c = is_number && !was_precedent_number; - const bool can_break_number_letter = is_number && !was_precedent_number && is_next_lower; - const bool can_break_letter_number = !is_number && was_precedent_number && (is_next_lower || is_next_number); + const bool cond_a = is_prev_lower && is_curr_upper; // aA + const bool cond_b = (is_prev_upper || is_prev_digit) && is_curr_upper && is_next_lower; // AAa, 2Aa + const bool cond_c = is_prev_digit && is_curr_lower && is_next_lower; // 2aa + const bool cond_d = (is_prev_upper || is_prev_lower) && is_curr_digit; // A2, a2 - bool should_split = cond_a || cond_b || cond_c || can_break_number_letter || can_break_letter_number; - if (should_split) { + if (cond_a || cond_b || cond_c || cond_d) { new_string += this->substr(start_index, i - start_index) + "_"; start_index = i; } } new_string += this->substr(start_index, this->size() - start_index); - return lowercase ? new_string.to_lower() : new_string; + return new_string.to_lower(); +} + +String String::capitalize() const { + String aux = this->_camelcase_to_underscore().replace("_", " ").strip_edges(); + String cap; + for (int i = 0; i < aux.get_slice_count(" "); i++) { + String slice = aux.get_slicec(' ', i); + if (slice.length() > 0) { + slice[0] = _find_upper(slice[0]); + if (i > 0) { + cap += " "; + } + cap += slice; + } + } + + return cap; +} + +String String::to_camel_case() const { + String s = this->to_pascal_case(); + if (!s.is_empty()) { + s[0] = _find_lower(s[0]); + } + return s; +} + +String String::to_pascal_case() const { + return this->capitalize().replace(" ", ""); +} + +String String::to_snake_case() const { + return this->_camelcase_to_underscore().replace(" ", "_").strip_edges(); } String String::get_with_code_lines() const { diff --git a/core/string/ustring.h b/core/string/ustring.h index 2463fc35f7..31de7cc464 100644 --- a/core/string/ustring.h +++ b/core/string/ustring.h @@ -196,6 +196,7 @@ class String { 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; + String _camelcase_to_underscore() const; public: enum { @@ -335,7 +336,9 @@ public: static double to_float(const char32_t *p_str, const char32_t **r_end = nullptr); String capitalize() const; - String camelcase_to_underscore(bool lowercase = true) const; + String to_camel_case() const; + String to_pascal_case() const; + String to_snake_case() const; String get_with_code_lines() const; int get_slice_count(String p_splitter) const; diff --git a/core/variant/variant_call.cpp b/core/variant/variant_call.cpp index 8e73c8dfc0..8af2a09111 100644 --- a/core/variant/variant_call.cpp +++ b/core/variant/variant_call.cpp @@ -1506,6 +1506,9 @@ static void _register_variant_builtin_methods() { bind_method(String, repeat, sarray("count"), varray()); bind_method(String, insert, sarray("position", "what"), varray()); bind_method(String, capitalize, sarray(), varray()); + bind_method(String, to_camel_case, sarray(), varray()); + bind_method(String, to_pascal_case, sarray(), varray()); + bind_method(String, to_snake_case, sarray(), varray()); bind_method(String, split, sarray("delimiter", "allow_empty", "maxsplit"), varray(true, 0)); bind_method(String, rsplit, sarray("delimiter", "allow_empty", "maxsplit"), varray(true, 0)); bind_method(String, split_floats, sarray("delimiter", "allow_empty"), varray(true)); diff --git a/doc/classes/String.xml b/doc/classes/String.xml index c111528c06..316bb923b7 100644 --- a/doc/classes/String.xml +++ b/doc/classes/String.xml @@ -775,6 +775,12 @@ Converts the String (which is a character array) to ASCII/Latin-1 encoded [PackedByteArray] (which is an array of bytes). The conversion is faster compared to [method to_utf8_buffer], as this method assumes that all the characters in the String are ASCII/Latin-1 characters, unsupported characters are replaced with spaces. + + + + Returns the string converted to [code]camelCase[/code]. + + @@ -804,6 +810,18 @@ Returns the string converted to lowercase. + + + + Returns the string converted to [code]PascalCase[/code]. + + + + + + Returns the string converted to [code]snake_case[/code]. + + diff --git a/editor/connections_dialog.cpp b/editor/connections_dialog.cpp index 587c16c229..4a477cd0a1 100644 --- a/editor/connections_dialog.cpp +++ b/editor/connections_dialog.cpp @@ -753,22 +753,12 @@ void ConnectionsDock::_open_connection_dialog(TreeItem &p_item) { } Dictionary subst; - - String s = node_name.capitalize().replace(" ", ""); - subst["NodeName"] = s; - if (!s.is_empty()) { - s[0] = s.to_lower()[0]; - } - subst["nodeName"] = s; - subst["node_name"] = node_name.capitalize().replace(" ", "_").to_lower(); - - s = signal_name.capitalize().replace(" ", ""); - subst["SignalName"] = s; - if (!s.is_empty()) { - s[0] = s.to_lower()[0]; - } - subst["signalName"] = s; - subst["signal_name"] = signal_name.capitalize().replace(" ", "_").to_lower(); + subst["NodeName"] = node_name.to_pascal_case(); + subst["nodeName"] = node_name.to_camel_case(); + subst["node_name"] = node_name.to_snake_case(); + subst["SignalName"] = signal_name.to_pascal_case(); + subst["signalName"] = signal_name.to_camel_case(); + subst["signal_name"] = signal_name.to_snake_case(); String dst_method = String(EDITOR_GET("interface/editors/default_signal_callback_name")).format(subst); diff --git a/editor/editor_autoload_settings.cpp b/editor/editor_autoload_settings.cpp index 6d44654617..544b6c7141 100644 --- a/editor/editor_autoload_settings.cpp +++ b/editor/editor_autoload_settings.cpp @@ -164,7 +164,7 @@ void EditorAutoloadSettings::_autoload_add() { if (!fpath.ends_with("/")) { fpath = fpath.get_base_dir(); } - dialog->config("Node", fpath.path_join(vformat("%s.gd", autoload_add_name->get_text().camelcase_to_underscore())), false, false); + dialog->config("Node", fpath.path_join(vformat("%s.gd", autoload_add_name->get_text().to_snake_case())), false, false); dialog->popup_centered(); } else { if (autoload_add(autoload_add_name->get_text(), autoload_add_path->get_text())) { @@ -371,7 +371,7 @@ void EditorAutoloadSettings::_autoload_open(const String &fpath) { void EditorAutoloadSettings::_autoload_file_callback(const String &p_path) { // Convert the file name to PascalCase, which is the convention for classes in GDScript. - const String class_name = p_path.get_file().get_basename().capitalize().replace(" ", ""); + const String class_name = p_path.get_file().get_basename().to_pascal_case(); // If the name collides with a built-in class, prefix the name to make it possible to add without having to edit the name. // The prefix is subjective, but it provides better UX than leaving the Add button disabled :) @@ -580,7 +580,7 @@ void EditorAutoloadSettings::_script_created(Ref