diff --git a/Core/GDCore/Events/Parsers/ExpressionParser2.cpp b/Core/GDCore/Events/Parsers/ExpressionParser2.cpp index cf71b9abd3..e943398781 100644 --- a/Core/GDCore/Events/Parsers/ExpressionParser2.cpp +++ b/Core/GDCore/Events/Parsers/ExpressionParser2.cpp @@ -19,8 +19,10 @@ #include "GDCore/Project/Project.h" #include "GDCore/Tools/Localization.h" #include "GDCore/Tools/MakeUnique.h" +#include "GrammarTerminals.h" using namespace std; +using namespace gd::GrammarTerminals; namespace gd { diff --git a/Core/GDCore/Events/Parsers/ExpressionParser2.h b/Core/GDCore/Events/Parsers/ExpressionParser2.h index 6f2058ed99..782b274f15 100644 --- a/Core/GDCore/Events/Parsers/ExpressionParser2.h +++ b/Core/GDCore/Events/Parsers/ExpressionParser2.h @@ -18,6 +18,7 @@ #include "GDCore/String.h" #include "GDCore/Tools/Localization.h" #include "GDCore/Tools/MakeUnique.h" +#include "GrammarTerminals.h" namespace gd { class Expression; class ObjectsContainer; @@ -28,6 +29,8 @@ class ExpressionMetadata; namespace gd { +using namespace gd::GrammarTerminals; + /** \brief Parse an expression, returning a tree of node corresponding * to the parsed expression. * @@ -211,7 +214,7 @@ class GD_CORE_API ExpressionParser2 { } SkipIfChar(IsClosingParenthesis); return factor; - } else if (IsIdentifierAllowedChar()) { + } else if (CheckIfChar(IsAllowedInIdentifier)) { return Identifier(); } @@ -606,95 +609,6 @@ class GD_CORE_API ExpressionParser2 { return predicate(character); } - bool IsIdentifierAllowedChar() { - if (currentPosition >= expression.size()) return false; - gd::String::value_type character = expression[currentPosition]; - - // Quickly compare if the character is a number or ASCII character. - if ((character >= '0' && character <= '9') || - (character >= 'A' && character <= 'Z') || - (character >= 'a' && character <= 'z')) - return true; - - // Otherwise do the full check against separators forbidden in identifiers. - if (!IsParameterSeparator(character) && !IsDot(character) && - !IsQuote(character) && !IsBracket(character) && - !IsExpressionOperator(character) && !IsTermOperator(character)) { - return true; - } - - return false; - } - - static bool IsWhitespace(gd::String::value_type character) { - return character == ' ' || character == '\n' || character == '\r'; - } - - static bool IsParameterSeparator(gd::String::value_type character) { - return character == ','; - } - - static bool IsDot(gd::String::value_type character) { - return character == '.'; - } - - static bool IsQuote(gd::String::value_type character) { - return character == '"'; - } - - static bool IsBracket(gd::String::value_type character) { - return character == '(' || character == ')' || character == '[' || - character == ']' || character == '{' || character == '}'; - } - - static bool IsOpeningParenthesis(gd::String::value_type character) { - return character == '('; - } - - static bool IsClosingParenthesis(gd::String::value_type character) { - return character == ')'; - } - - static bool IsOpeningSquareBracket(gd::String::value_type character) { - return character == '['; - } - - static bool IsClosingSquareBracket(gd::String::value_type character) { - return character == ']'; - } - - static bool IsExpressionEndingChar(gd::String::value_type character) { - return character == ',' || IsClosingParenthesis(character) || - IsClosingSquareBracket(character); - } - - static bool IsExpressionOperator(gd::String::value_type character) { - return character == '+' || character == '-' || character == '<' || - character == '>' || character == '?' || character == '^' || - character == '=' || character == '\\' || character == ':' || - character == '!'; - } - - static bool IsUnaryOperator(gd::String::value_type character) { - return character == '+' || character == '-'; - } - - static bool IsTermOperator(gd::String::value_type character) { - return character == '/' || character == '*'; - } - - static bool IsNumberFirstChar(gd::String::value_type character) { - return character == '.' || (character >= '0' && character <= '9'); - } - - static bool IsNonZeroDigit(gd::String::value_type character) { - return (character >= '1' && character <= '9'); - } - - static bool IsZeroDigit(gd::String::value_type character) { - return character == '0'; - } - bool IsNamespaceSeparator() { // Namespace separator is a special kind of delimiter as it is 2 characters // long @@ -715,7 +629,7 @@ class GD_CORE_API ExpressionParser2 { gd::String name; size_t startPosition = currentPosition; while (currentPosition < expression.size() && - (IsIdentifierAllowedChar() + (CheckIfChar(IsAllowedInIdentifier) // Allow whitespace in identifier name for compatibility || expression[currentPosition] == ' ')) { name += expression[currentPosition]; diff --git a/Core/GDCore/Events/Parsers/GrammarTerminals.h b/Core/GDCore/Events/Parsers/GrammarTerminals.h new file mode 100644 index 0000000000..65c3c7e993 --- /dev/null +++ b/Core/GDCore/Events/Parsers/GrammarTerminals.h @@ -0,0 +1,107 @@ +#pragma once +#include "GDCore/String.h" + +namespace gd { + +/** + * Contains functions to handle the grammar of the expressions accepted by GDevelop. + */ +namespace GrammarTerminals { + +inline bool IsWhitespace(gd::String::value_type character) { + return character == ' ' || character == '\n' || character == '\r'; +} + +inline bool IsParameterSeparator(gd::String::value_type character) { + return character == ','; +} + +inline bool IsDot(gd::String::value_type character) { return character == '.'; } + +inline bool IsQuote(gd::String::value_type character) { + return character == '"'; +} + +inline bool IsBracket(gd::String::value_type character) { + return character == '(' || character == ')' || character == '[' || + character == ']' || character == '{' || character == '}'; +} + +inline bool IsOpeningParenthesis(gd::String::value_type character) { + return character == '('; +} + +inline bool IsClosingParenthesis(gd::String::value_type character) { + return character == ')'; +} + +inline bool IsOpeningSquareBracket(gd::String::value_type character) { + return character == '['; +} + +inline bool IsClosingSquareBracket(gd::String::value_type character) { + return character == ']'; +} + +inline bool IsExpressionEndingChar(gd::String::value_type character) { + return character == ',' || IsClosingParenthesis(character) || + IsClosingSquareBracket(character); +} + +inline bool IsExpressionOperator(gd::String::value_type character) { + return character == '+' || character == '-' || character == '<' || + character == '>' || character == '?' || character == '^' || + character == '=' || character == '\\' || character == ':' || + character == '!'; +} + +inline bool IsUnaryOperator(gd::String::value_type character) { + return character == '+' || character == '-'; +} + +inline bool IsTermOperator(gd::String::value_type character) { + return character == '/' || character == '*'; +} + +inline bool IsNumberFirstChar(gd::String::value_type character) { + return character == '.' || (character >= '0' && character <= '9'); +} + +inline bool IsNonZeroDigit(gd::String::value_type character) { + return (character >= '1' && character <= '9'); +} + +inline bool IsZeroDigit(gd::String::value_type character) { + return character == '0'; +} + +/** + * Check if the given character can be used in an identifier. This is + * any unicode character, except for: + * `, . " () [] {} + - < > ? ^ = \ : ! / *` and whitespaces (space, line break, carriage return). + * + * This is loosely based on what is allowed in languages like JavaScript + * (see https://mathiasbynens.be/notes/javascript-properties), without support + * for unicode escape syntax, and allowing all unicode ranges. The only + * disallowed characters are the one used for the grammar. + */ +inline bool IsAllowedInIdentifier(gd::String::value_type character) { + // Quickly compare if the character is a number or ASCII character. + if ((character >= '0' && character <= '9') || + (character >= 'A' && character <= 'Z') || + (character >= 'a' && character <= 'z')) + return true; + + // Otherwise do the full check against separators forbidden in identifiers. + if (!IsParameterSeparator(character) && !IsDot(character) && + !IsQuote(character) && !IsBracket(character) && + !IsExpressionOperator(character) && !IsTermOperator(character) && + !IsWhitespace(character)) { + return true; + } + + return false; +} + +} // namespace GrammarTerminals +} // namespace gd \ No newline at end of file diff --git a/Core/GDCore/Project/Project.cpp b/Core/GDCore/Project/Project.cpp index 2a55f8b9c1..d273df126e 100644 --- a/Core/GDCore/Project/Project.cpp +++ b/Core/GDCore/Project/Project.cpp @@ -15,6 +15,7 @@ #include #include "GDCore/CommonTools.h" +#include "GDCore/Events/Parsers/GrammarTerminals.h" #include "GDCore/Extensions/Metadata/ExpressionMetadata.h" #include "GDCore/Extensions/Metadata/MetadataProvider.h" #include "GDCore/Extensions/Platform.h" @@ -49,6 +50,11 @@ using namespace std; namespace gd { +// By default, disallow unicode in identifiers, but this can be set to true +// by the IDE. In the future, this will be set to true by default, keeping backward compatibility. +// We keep it disabled by default to progressively ask users to test it in real projects. +bool Project::allowUsageOfUnicodeIdentifierNames = false; + Project::Project() : name(_("Project")), version("1.0.0"), @@ -630,8 +636,10 @@ void Project::UnserializeFrom(const SerializerElement& element) { SetAdaptGameResolutionAtRuntime( propElement.GetBoolAttribute("adaptGameResolutionAtRuntime", false)); SetSizeOnStartupMode(propElement.GetStringAttribute("sizeOnStartupMode", "")); - SetAntialiasingMode(propElement.GetStringAttribute("antialiasingMode", "MSAA")); - SetAntialisingEnabledOnMobile(propElement.GetBoolAttribute("antialisingEnabledOnMobile", false)); + SetAntialiasingMode( + propElement.GetStringAttribute("antialiasingMode", "MSAA")); + SetAntialisingEnabledOnMobile( + propElement.GetBoolAttribute("antialisingEnabledOnMobile", false)); SetProjectUuid(propElement.GetStringAttribute("projectUuid", "")); SetAuthor(propElement.GetChild("author", 0, "Auteur").GetValue().GetString()); SetPackageName(propElement.GetStringAttribute("packageName")); @@ -887,7 +895,8 @@ void Project::SerializeTo(SerializerElement& element) const { adaptGameResolutionAtRuntime); propElement.SetAttribute("sizeOnStartupMode", sizeOnStartupMode); propElement.SetAttribute("antialiasingMode", antialiasingMode); - propElement.SetAttribute("antialisingEnabledOnMobile", isAntialisingEnabledOnMobile); + propElement.SetAttribute("antialisingEnabledOnMobile", + isAntialisingEnabledOnMobile); propElement.SetAttribute("projectUuid", projectUuid); propElement.SetAttribute("folderProject", folderProject); propElement.SetAttribute("packageName", packageName); @@ -993,14 +1002,28 @@ void Project::SerializeTo(SerializerElement& element) const { externalSourceFilesElement.AddChild("sourceFile")); } +void Project::AllowUsageOfUnicodeIdentifierNames(bool enable) { + allowUsageOfUnicodeIdentifierNames = enable; +} + bool Project::IsNameSafe(const gd::String& name) { if (name.empty()) return false; if (isdigit(name[0])) return false; - gd::String allowedCharacters = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; - return !(name.find_first_not_of(allowedCharacters) != gd::String::npos); + if (!allowUsageOfUnicodeIdentifierNames) { + gd::String legacyAllowedCharacters = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; + return !(name.find_first_not_of(legacyAllowedCharacters) != gd::String::npos); + } else { + for (auto character : name) { + if (!GrammarTerminals::IsAllowedInIdentifier(character)) { + return false; + } + } + + return true; + } } gd::String Project::GetSafeName(const gd::String& name) { @@ -1010,12 +1033,21 @@ gd::String Project::GetSafeName(const gd::String& name) { if (isdigit(name[0])) newName = "_" + newName; - gd::String allowedCharacters = + gd::String legacyAllowedCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; - + for (size_t i = 0;i < newName.size();++i) { + // Note that iterating on the characters is not super efficient (O(n^2), which + // could be avoided with an iterator), but this function is not critical for performance + // (only used to generate a name when a user creates a new entity or rename one). + auto character = newName[i]; + bool isAllowed = + allowUsageOfUnicodeIdentifierNames + ? GrammarTerminals::IsAllowedInIdentifier(character) + : legacyAllowedCharacters.find(character) != gd::String::npos; + // Replace all unallowed letters by an underscore. - if (allowedCharacters.find_first_of(std::u32string(1, newName[i])) == gd::String::npos) { + if (!isAllowed) { newName.replace(i, 1, '_'); } } diff --git a/Core/GDCore/Project/Project.h b/Core/GDCore/Project/Project.h index e3026c4c9a..7a964cc10e 100644 --- a/Core/GDCore/Project/Project.h +++ b/Core/GDCore/Project/Project.h @@ -975,10 +975,24 @@ class GD_CORE_API Project : public ObjectsContainer { ///@} - /** \name Other + /** \name Identifier names */ ///@{ + /** + * Check if unicode names are allowed in identifier names. + * \see IsNameSafe + * \see GetSafeName + */ + static bool IsUsageOfUnicodeIdentifierNamesAllowed() { return allowUsageOfUnicodeIdentifierNames; }; + + /** + * Set if unicode names are allowed in identifier names. + * \see IsNameSafe + * \see GetSafeName + */ + static void AllowUsageOfUnicodeIdentifierNames(bool enable); + /** * Return true if \a name is valid (can be used safely for an object, * behavior, events function name, etc...). @@ -989,7 +1003,7 @@ class GD_CORE_API Project : public ObjectsContainer { * Return a name, based on the one passed in parameter, that can be safely used * for an object, behavior, events function name, etc... */ - static gd::String GetSafeName(const gd::String& name); + static gd::String GetSafeName(const gd::String& name); ///@} /** \name External source files @@ -1124,6 +1138,8 @@ class GD_CORE_API Project : public ObjectsContainer { ///< time the project was saved. mutable unsigned int gdBuildVersion; ///< The GD build version used the last ///< time the project was saved. + + static bool allowUsageOfUnicodeIdentifierNames; }; } // namespace gd diff --git a/Core/tests/ExpressionParser2.cpp b/Core/tests/ExpressionParser2.cpp index 41c9eb9bc8..de63bbaad3 100644 --- a/Core/tests/ExpressionParser2.cpp +++ b/Core/tests/ExpressionParser2.cpp @@ -26,7 +26,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { auto &myGroup = layout1.GetObjectGroups().InsertNew("MyGroup", 0); myGroup.AddObject(myObject.GetName()); - + layout1.InsertNewObject(project, "MyExtension::Sprite", "MySpriteObject", 1); layout1.InsertNewObject(project, "MyExtension::FakeObjectWithUnsupportedCapability", @@ -340,6 +340,20 @@ TEST_CASE("ExpressionParser2", "[common][events]") { "Cannot find an expression with this name: Idontexist\n" "Double check that you've not made any typo in the name."); } + { + auto node = parser.ParseExpression("๐Ÿ…ธ๐Ÿ…ณ๐Ÿ…พ๐Ÿ…ฝ๐Ÿ†ƒ๐Ÿ…ด๐Ÿ†‡๐Ÿ…ธ๐Ÿ†‚๐Ÿ†ƒ๐Ÿ˜„(\"hello\""); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, project, layout1, "string"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 2); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "The list of parameters is not terminated. Add a closing " + "parenthesis to end the parameters."); + REQUIRE(validator.GetFatalErrors()[1]->GetMessage() == + "Cannot find an expression with this name: ๐Ÿ…ธ๐Ÿ…ณ๐Ÿ…พ๐Ÿ…ฝ๐Ÿ†ƒ๐Ÿ…ด๐Ÿ†‡๐Ÿ…ธ๐Ÿ†‚๐Ÿ†ƒ๐Ÿ˜„\n" + "Double check that you've not made any typo in the name."); + } { auto node = parser.ParseExpression("=\"test\""); REQUIRE(node != nullptr); @@ -694,6 +708,17 @@ TEST_CASE("ExpressionParser2", "[common][events]") { "You must enter a number."); REQUIRE(validator.GetFatalErrors()[0]->GetStartPosition() == 0); } + { + auto node = parser.ParseExpression("ab๐Ÿ˜Šd"); + REQUIRE(node != nullptr); + + gd::ExpressionValidator validator(platform, project, layout1, "number"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 1); + REQUIRE(validator.GetFatalErrors()[0]->GetMessage() == + "You must enter a number."); + REQUIRE(validator.GetFatalErrors()[0]->GetStartPosition() == 0); + } { auto node = parser.ParseExpression("abcd[0]"); REQUIRE(node != nullptr); @@ -917,6 +942,26 @@ TEST_CASE("ExpressionParser2", "[common][events]") { node->Visit(validator); REQUIRE(validator.GetFatalErrors().size() == 0); } + { + auto node = parser.ParseExpression("๐Ÿ˜…"); + REQUIRE(node != nullptr); + auto &identifierNode = dynamic_cast(*node); + REQUIRE(identifierNode.identifierName == "๐Ÿ˜…"); + + gd::ExpressionValidator validator(platform, project, layout1, "object"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 0); + } + { + auto node = parser.ParseExpression("ไธญๆ–‡"); + REQUIRE(node != nullptr); + auto &identifierNode = dynamic_cast(*node); + REQUIRE(identifierNode.identifierName == "ไธญๆ–‡"); + + gd::ExpressionValidator validator(platform, project, layout1, "object"); + node->Visit(validator); + REQUIRE(validator.GetFatalErrors().size() == 0); + } { auto node = parser.ParseExpression("Hello World 1"); @@ -1092,6 +1137,46 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(textNode.text == "2"); REQUIRE(identifierNode.identifierName == "three"); } + SECTION("identifier parameter (unicode)") { + auto node = parser.ParseExpression( + "WhateverObject.WhateverBehavior::WhateverFunction(1, \"2\", ๐Ÿ˜„)"); + REQUIRE(node != nullptr); + auto &functionNode = dynamic_cast(*node); + REQUIRE(functionNode.functionName == "WhateverFunction"); + REQUIRE(functionNode.objectName == "WhateverObject"); + REQUIRE(functionNode.behaviorName == "WhateverBehavior"); + REQUIRE(functionNode.parameters.size() == 3); + auto &numberNode = + dynamic_cast(*functionNode.parameters[0]); + auto &textNode = + dynamic_cast(*functionNode.parameters[1]); + auto &identifierNode = + dynamic_cast(*functionNode.parameters[2]); + + REQUIRE(numberNode.number == "1"); + REQUIRE(textNode.text == "2"); + REQUIRE(identifierNode.identifierName == "๐Ÿ˜„"); + } + SECTION("unicode for object, behavior and function") { + auto node = parser.ParseExpression( + "๐Ÿงธ.๐Ÿ—ฃ๏ธ::๐Ÿ””(1, \"2\", ๐Ÿ˜„)"); + REQUIRE(node != nullptr); + auto &functionNode = dynamic_cast(*node); + REQUIRE(functionNode.functionName == "๐Ÿ””"); + REQUIRE(functionNode.objectName == "๐Ÿงธ"); + REQUIRE(functionNode.behaviorName == "๐Ÿ—ฃ๏ธ"); + REQUIRE(functionNode.parameters.size() == 3); + auto &numberNode = + dynamic_cast(*functionNode.parameters[0]); + auto &textNode = + dynamic_cast(*functionNode.parameters[1]); + auto &identifierNode = + dynamic_cast(*functionNode.parameters[2]); + + REQUIRE(numberNode.number == "1"); + REQUIRE(textNode.text == "2"); + REQUIRE(identifierNode.identifierName == "๐Ÿ˜„"); + } } SECTION("Valid function calls ('number|string' type)") { @@ -1669,7 +1754,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") { auto &identifierNode = dynamic_cast(*node); REQUIRE(identifierNode.identifierName == "myVariable"); REQUIRE(identifierNode.childIdentifierName == "myChild"); - + gd::ExpressionValidator validator(platform, project, layout1, "scenevar"); node->Visit(validator); REQUIRE(validator.GetFatalErrors().size() == 0); @@ -1766,14 +1851,14 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(identifierObject2Node.identifierName == "MyObject2"); REQUIRE(variable1Node.identifierName == "MyVar1"); REQUIRE(variable2Node.identifierName == "MyVar2"); - + auto variable1ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName( platform, project, layout1, "", variable1Node); REQUIRE(variable1ObjectName == "MyObject1"); auto variable2ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName( platform, project, layout1, "", variable2Node); REQUIRE(variable2ObjectName == "MyObject2"); - + gd::ExpressionValidator validator(platform, project, layout1, "string"); node->Visit(validator); REQUIRE(validator.GetFatalErrors().size() == 0); @@ -1797,14 +1882,14 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(identifierObject1Node.identifierName == "MyObject1"); REQUIRE(variable1Node.identifierName == "MyVar1"); REQUIRE(variable2Node.identifierName == "MyVar2"); - + auto variable1ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName( platform, project, layout1, "", variable1Node); REQUIRE(variable1ObjectName == "MyObject1"); auto variable2ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName( platform, project, layout1, "", variable2Node); REQUIRE(variable2ObjectName == "MyObject1"); - + gd::ExpressionValidator validator(platform, project, layout1, "string"); node->Visit(validator); REQUIRE(validator.GetFatalErrors().size() == 0); @@ -1821,11 +1906,11 @@ TEST_CASE("ExpressionParser2", "[common][events]") { dynamic_cast(*functionNode.parameters[0]); REQUIRE(variable1Node.identifierName == "MyVar1"); - + auto variable1ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName( platform, project, layout1, "MySpriteObject", variable1Node); REQUIRE(variable1ObjectName == "MySpriteObject"); - + gd::ExpressionValidator validator(platform, project, layout1, "number"); node->Visit(validator); REQUIRE(validator.GetFatalErrors().size() == 0); @@ -1843,11 +1928,11 @@ TEST_CASE("ExpressionParser2", "[common][events]") { REQUIRE(variable1Node.identifierName == "MyVar1"); REQUIRE(variable1Node.childIdentifierName == "MyChild"); - + auto variable1ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName( platform, project, layout1, "MySpriteObject", variable1Node); REQUIRE(variable1ObjectName == "MySpriteObject"); - + gd::ExpressionValidator validator(platform, project, layout1, "number"); node->Visit(validator); REQUIRE(validator.GetFatalErrors().size() == 0); diff --git a/GDJS/GDJS/Events/CodeGeneration/MetadataDeclarationHelper.cpp b/GDJS/GDJS/Events/CodeGeneration/MetadataDeclarationHelper.cpp index 2f34f1b8bc..dcffc5ddda 100644 --- a/GDJS/GDJS/Events/CodeGeneration/MetadataDeclarationHelper.cpp +++ b/GDJS/GDJS/Events/CodeGeneration/MetadataDeclarationHelper.cpp @@ -1352,7 +1352,7 @@ void MetadataDeclarationHelper::DeclareEventsFunctionParameters( } gd::String MetadataDeclarationHelper::GetExtensionCodeNamespacePrefix( - const gd::EventsFunctionsExtension eventsFunctionsExtension) { + const gd::EventsFunctionsExtension &eventsFunctionsExtension) { return "gdjs.evtsExt__" + EventsCodeNameMangler::GetMangledName( eventsFunctionsExtension.GetName()); } diff --git a/GDJS/GDJS/Events/CodeGeneration/MetadataDeclarationHelper.h b/GDJS/GDJS/Events/CodeGeneration/MetadataDeclarationHelper.h index baeacd5143..f70e6323b3 100644 --- a/GDJS/GDJS/Events/CodeGeneration/MetadataDeclarationHelper.h +++ b/GDJS/GDJS/Events/CodeGeneration/MetadataDeclarationHelper.h @@ -66,10 +66,30 @@ public: const gd::EventsBasedObject &eventsBasedObject, std::map &objectMethodMangledNames); + /** Generate the namespace prefix for an extension. */ + static gd::String GetExtensionCodeNamespacePrefix( + const gd::EventsFunctionsExtension &eventsFunctionsExtension); + + /** Generate the fully qualified name for a free function. */ static gd::String GetFreeFunctionCodeName( const gd::EventsFunctionsExtension &eventsFunctionsExtension, const gd::EventsFunction &eventsFunction); + /** Generate the namespace for a free function. */ + static gd::String + GetFreeFunctionCodeNamespace(const gd::EventsFunction &eventsFunction, + const gd::String &codeNamespacePrefix); + + /** Generate the namespace for a behavior function. */ + static gd::String GetBehaviorFunctionCodeNamespace( + const gd::EventsBasedBehavior &eventsBasedBehavior, + const gd::String &codeNamespacePrefix); + + /** Generate the namespace for an object function. */ + static gd::String + GetObjectFunctionCodeNamespace(const gd::EventsBasedObject &eventsBasedObject, + const gd::String &codeNamespacePrefix); + /** * Declare an extension from an events based extension. */ @@ -292,24 +312,6 @@ private: gd::MultipleInstructionMetadata &multipleInstructionMetadata, const int userDefinedFirstParameterIndex); - static gd::String GetExtensionCodeNamespacePrefix( - const gd::EventsFunctionsExtension eventsFunctionsExtension); - - /** Generate the namespace for a free function. */ - static gd::String - GetFreeFunctionCodeNamespace(const gd::EventsFunction &eventsFunction, - const gd::String &codeNamespacePrefix); - - /** Generate the namespace for a behavior function. */ - static gd::String GetBehaviorFunctionCodeNamespace( - const gd::EventsBasedBehavior &eventsBasedBehavior, - const gd::String &codeNamespacePrefix); - - /** Generate the namespace for an object function. */ - static gd::String - GetObjectFunctionCodeNamespace(const gd::EventsBasedObject &eventsBasedObject, - const gd::String &codeNamespacePrefix); - static gd::String RemoveTrailingDot(const gd::String &description); static gd::String diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl index d799cff337..554468d924 100644 --- a/GDevelop.js/Bindings/Bindings.idl +++ b/GDevelop.js/Bindings/Bindings.idl @@ -521,6 +521,8 @@ interface Project { void SerializeTo([Ref] SerializerElement element); void UnserializeFrom([Const, Ref] SerializerElement element); + boolean STATIC_IsUsageOfUnicodeIdentifierNamesAllowed(); + void STATIC_AllowUsageOfUnicodeIdentifierNames(boolean enable); boolean STATIC_IsNameSafe([Const] DOMString name); [Const, Value] DOMString STATIC_GetSafeName([Const] DOMString name); @@ -1291,7 +1293,7 @@ interface AbstractFunctionMetadata { [Ref] AbstractFunctionMetadata SetDefaultValue([Const] DOMString defaultValue); [Ref] AbstractFunctionMetadata SetParameterLongDescription([Const] DOMString longDescription); [Ref] AbstractFunctionMetadata SetParameterExtraInfo([Const] DOMString extraInfo); - + [Ref] AbstractFunctionMetadata SetHidden(); [Ref] AbstractFunctionMetadata SetPrivate(); @@ -1354,7 +1356,7 @@ interface InstructionMetadata { [Ref] InstructionMetadata MarkAsComplex(); [Ref] InstructionMetadata GetCodeExtraInformation(); - + [Ref] InstructionMetadata SetFunctionName([Const] DOMString functionName_); [Ref] InstructionMetadata SetAsyncFunctionName([Const] DOMString functionName_); [Const, Ref] DOMString GetFunctionName(); @@ -1415,7 +1417,7 @@ interface ExpressionMetadata { [Const, Ref] DOMString GetRequiredBaseObjectCapability(); [Ref] ExpressionMetadata GetCodeExtraInformation(); - + [Ref] ExpressionMetadata SetFunctionName([Const] DOMString functionName); [Const, Ref] DOMString GetFunctionName(); [Ref] ExpressionMetadata SetStatic(); @@ -3397,7 +3399,7 @@ interface JsCodeEvent { [Prefix="gdjs::"] interface MetadataDeclarationHelper { void MetadataDeclarationHelper(); - + void STATIC_DeclareExtension( [Ref] PlatformExtension extension, [Const, Ref] EventsFunctionsExtension eventsFunctionsExtension); @@ -3422,10 +3424,25 @@ interface MetadataDeclarationHelper { [Const, Ref] EventsBasedObject eventsBasedObject, [Ref] MapStringString objectMethodMangledNames); + [Const, Value] DOMString STATIC_GetExtensionCodeNamespacePrefix( + [Const, Ref] EventsFunctionsExtension eventsFunctionsExtension); + [Const, Value] DOMString STATIC_GetFreeFunctionCodeName( [Const, Ref] EventsFunctionsExtension eventsFunctionsExtension, [Const, Ref] EventsFunction eventsFunction); + [Const, Value] DOMString STATIC_GetFreeFunctionCodeNamespace( + [Const, Ref] EventsFunction eventsFunction, + [Const] DOMString codeNamespacePrefix); + + [Const, Value] DOMString STATIC_GetBehaviorFunctionCodeNamespace( + [Const, Ref] EventsBasedBehavior eventsBasedBehavior, + [Const] DOMString codeNamespacePrefix); + + [Const, Value] DOMString STATIC_GetObjectFunctionCodeNamespace( + [Const, Ref] EventsBasedObject eventsBasedObject, + [Const] DOMString codeNamespacePrefix); + boolean STATIC_IsBehaviorLifecycleEventsFunction([Const] DOMString functionName); boolean STATIC_IsObjectLifecycleEventsFunction([Const] DOMString functionName); boolean STATIC_IsExtensionLifecycleEventsFunction([Const] DOMString functionName); diff --git a/GDevelop.js/Bindings/Wrapper.cpp b/GDevelop.js/Bindings/Wrapper.cpp index 8b92b49841..24b2b15689 100644 --- a/GDevelop.js/Bindings/Wrapper.cpp +++ b/GDevelop.js/Bindings/Wrapper.cpp @@ -522,6 +522,8 @@ typedef ExtensionAndMetadata ExtensionAndExpressionMetadata; #define STATIC_CreateNewGDJSProject CreateNewGDJSProject #define STATIC_InitializePlatforms InitializePlatforms +#define STATIC_IsUsageOfUnicodeIdentifierNamesAllowed IsUsageOfUnicodeIdentifierNamesAllowed +#define STATIC_AllowUsageOfUnicodeIdentifierNames AllowUsageOfUnicodeIdentifierNames #define STATIC_IsNameSafe IsNameSafe #define STATIC_GetSafeName GetSafeName #define STATIC_ToJSON ToJSON @@ -698,7 +700,11 @@ typedef ExtensionAndMetadata ExtensionAndExpressionMetadata; #define STATIC_DeclareExtension DeclareExtension #define STATIC_GenerateBehaviorMetadata GenerateBehaviorMetadata #define STATIC_GenerateObjectMetadata GenerateObjectMetadata +#define STATIC_GetExtensionCodeNamespacePrefix GetExtensionCodeNamespacePrefix #define STATIC_GetFreeFunctionCodeName GetFreeFunctionCodeName +#define STATIC_GetFreeFunctionCodeNamespace GetFreeFunctionCodeNamespace +#define STATIC_GetBehaviorFunctionCodeNamespace GetBehaviorFunctionCodeNamespace +#define STATIC_GetObjectFunctionCodeNamespace GetObjectFunctionCodeNamespace #define STATIC_IsBehaviorLifecycleEventsFunction IsBehaviorLifecycleEventsFunction #define STATIC_IsObjectLifecycleEventsFunction IsObjectLifecycleEventsFunction #define STATIC_IsExtensionLifecycleEventsFunction IsExtensionLifecycleEventsFunction diff --git a/GDevelop.js/__tests__/Core.js b/GDevelop.js/__tests__/Core.js index 006746e87c..7c8ee696ec 100644 --- a/GDevelop.js/__tests__/Core.js +++ b/GDevelop.js/__tests__/Core.js @@ -99,7 +99,8 @@ describe('libGD.js', function () { expect(project.hasExternalLayoutNamed('My layout')).toBe(false); }); - it('should validate object names', function () { + it('should validate object names (legacy)', function () { + gd.Project.allowUsageOfUnicodeIdentifierNames(false); expect(gd.Project.isNameSafe('ThisNameIs_Ok_123')).toBe(true); expect(gd.Project.isNameSafe('ThisName IsNot_Ok_123')).toBe(false); expect(gd.Project.isNameSafe('ThisNameIsNot_Ok!')).toBe(false); @@ -113,6 +114,25 @@ describe('libGD.js', function () { expect(gd.Project.getSafeName('9')).toBe('_9'); }); + it('should validate object names (unicode)', function () { + gd.Project.allowUsageOfUnicodeIdentifierNames(true); + expect(gd.Project.isNameSafe('ThisNameIs_Ok_123')).toBe(true); + expect(gd.Project.isNameSafe('ThisNameIs_๐Ÿ‘_123')).toBe(true); + expect(gd.Project.isNameSafe('ThisName IsNot_Ok_123')).toBe(false); + expect(gd.Project.isNameSafe('ThisName()IsNot_Ok_123')).toBe(false); + expect(gd.Project.isNameSafe('ThisNameIsNot_Ok!')).toBe(false); + expect(gd.Project.isNameSafe('1ThisNameIsNot_Ok_123')).toBe(false); + expect(gd.Project.getSafeName('ThisNameIs_Ok_123')).toBe('ThisNameIs_Ok_123'); + expect(gd.Project.getSafeName('ThisNameIs_๐Ÿ‘_123')).toBe('ThisNameIs_๐Ÿ‘_123'); + expect(gd.Project.getSafeName('ThisName IsNot_Ok_123')).toBe('ThisName_IsNot_Ok_123'); + expect(gd.Project.getSafeName('ThisName()IsNot_Ok_123')).toBe('ThisName__IsNot_Ok_123'); + expect(gd.Project.getSafeName('ThisNameIsNot_Ok!')).toBe('ThisNameIsNot_Ok_'); + expect(gd.Project.getSafeName('1ThisNameIsNot_Ok_123')).toBe('_1ThisNameIsNot_Ok_123'); + expect(gd.Project.getSafeName('ๅฎ˜่ฏ name')).toBe('ๅฎ˜่ฏ_name'); + expect(gd.Project.getSafeName('')).toBe('Unnamed'); + expect(gd.Project.getSafeName('9')).toBe('_9'); + }); + it('should have a list of extensions', function () { expect( gd.UsedExtensionsFinder.scanProject(project) diff --git a/GDevelop.js/__tests__/MetadataDeclarationHelper.js b/GDevelop.js/__tests__/MetadataDeclarationHelper.js index 98ddf4758d..383e0aeaed 100644 --- a/GDevelop.js/__tests__/MetadataDeclarationHelper.js +++ b/GDevelop.js/__tests__/MetadataDeclarationHelper.js @@ -45,16 +45,16 @@ describe('MetadataDeclarationHelper', () => { project.delete(); }); - it('can create metadata for free actions with an underscore', () => { + it('can create metadata for free actions with an underscore and unicode characters', () => { const extension = new gd.PlatformExtension(); const project = new gd.Project(); const eventExtension = project.insertNewEventsFunctionsExtension( - 'MyExtension', + 'My๐ŸงฉExtension', 0 ); const eventFunction = eventExtension.insertNewEventsFunction( - 'My_Function', + 'My_๐Ÿ“Function', 0 ); eventFunction.setFunctionType(gd.EventsFunction.Action); @@ -68,10 +68,10 @@ describe('MetadataDeclarationHelper', () => { ); metadataDeclarationHelper.delete(); - expect(extension.getAllActions().has('My_Function')).toBe(true); - const action = extension.getAllActions().get('My_Function'); + expect(extension.getAllActions().has('My_๐Ÿ“Function')).toBe(true); + const action = extension.getAllActions().get('My_๐Ÿ“Function'); expect(action.getFunctionName()).toBe( - 'gdjs.evtsExt__MyExtension__My_Function.func' + 'gdjs.evtsExt__My_129513Extension__My__128221Function.func' ); extension.delete(); diff --git a/GDevelop.js/types/gdmetadatadeclarationhelper.js b/GDevelop.js/types/gdmetadatadeclarationhelper.js index c1fc0ce966..7c7263c3bf 100644 --- a/GDevelop.js/types/gdmetadatadeclarationhelper.js +++ b/GDevelop.js/types/gdmetadatadeclarationhelper.js @@ -5,7 +5,11 @@ declare class gdMetadataDeclarationHelper { generateFreeFunctionMetadata(project: gdProject, extension: gdPlatformExtension, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsFunction: gdEventsFunction): gdAbstractFunctionMetadata; static generateBehaviorMetadata(project: gdProject, extension: gdPlatformExtension, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedBehavior: gdEventsBasedBehavior, behaviorMethodMangledNames: gdMapStringString): gdBehaviorMetadata; static generateObjectMetadata(project: gdProject, extension: gdPlatformExtension, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, objectMethodMangledNames: gdMapStringString): gdObjectMetadata; + static getExtensionCodeNamespacePrefix(eventsFunctionsExtension: gdEventsFunctionsExtension): string; static getFreeFunctionCodeName(eventsFunctionsExtension: gdEventsFunctionsExtension, eventsFunction: gdEventsFunction): string; + static getFreeFunctionCodeNamespace(eventsFunction: gdEventsFunction, codeNamespacePrefix: string): string; + static getBehaviorFunctionCodeNamespace(eventsBasedBehavior: gdEventsBasedBehavior, codeNamespacePrefix: string): string; + static getObjectFunctionCodeNamespace(eventsBasedObject: gdEventsBasedObject, codeNamespacePrefix: string): string; static isBehaviorLifecycleEventsFunction(functionName: string): boolean; static isObjectLifecycleEventsFunction(functionName: string): boolean; static isExtensionLifecycleEventsFunction(functionName: string): boolean; diff --git a/GDevelop.js/types/gdproject.js b/GDevelop.js/types/gdproject.js index 1657ee5308..7e470d27e3 100644 --- a/GDevelop.js/types/gdproject.js +++ b/GDevelop.js/types/gdproject.js @@ -104,6 +104,8 @@ declare class gdProject extends gdObjectsContainer { exposeResources(worker: gdArbitraryResourceWorker): void; serializeTo(element: gdSerializerElement): void; unserializeFrom(element: gdSerializerElement): void; + static isUsageOfUnicodeIdentifierNamesAllowed(): boolean; + static allowUsageOfUnicodeIdentifierNames(enable: boolean): void; static isNameSafe(name: string): boolean; static getSafeName(name: string): string; getTypeOfBehavior(layout: gdLayout, name: string, searchInGroups: boolean): string; diff --git a/newIDE/app/src/EventsFunctionsExtensionsLoader/index.js b/newIDE/app/src/EventsFunctionsExtensionsLoader/index.js index 304ebbfafd..29325c167f 100644 --- a/newIDE/app/src/EventsFunctionsExtensionsLoader/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionsLoader/index.js @@ -1,7 +1,6 @@ // @flow import { type I18n as I18nType } from '@lingui/core'; import { mapVector, mapFor } from '../Utils/MapFor'; -import { caseSensitiveSlug } from '../Utils/CaseSensitiveSlug'; const gd: libGDevelop = global.gd; @@ -32,56 +31,10 @@ type OptionsForGeneration = { }; type CodeGenerationContext = {| - codeNamespacePrefix: string, + codeNamespacePrefix: string, // TODO: could this reworked to avoid this entirely? extensionIncludeFiles: Array, |}; -const mangleName = (name: string) => { - return caseSensitiveSlug(name, '_', []); -}; - -const getExtensionCodeNamespacePrefix = ( - eventsFunctionsExtension: gdEventsFunctionsExtension -) => { - return 'gdjs.evtsExt__' + mangleName(eventsFunctionsExtension.getName()); -}; - -/** Generate the namespace for a free function. */ -const getFreeFunctionCodeNamespace = ( - eventsFunction: gdEventsFunction, - codeNamespacePrefix: string -) => { - return codeNamespacePrefix + '__' + mangleName(eventsFunction.getName()); -}; - -export const getFreeFunctionCodeName = ( - eventsFunctionsExtension: gdEventsFunctionsExtension, - eventsFunction: gdEventsFunction -) => { - return ( - getFreeFunctionCodeNamespace( - eventsFunction, - getExtensionCodeNamespacePrefix(eventsFunctionsExtension) - ) + '.func' - ); -}; - -/** Generate the namespace for a behavior function. */ -const getBehaviorFunctionCodeNamespace = ( - eventsBasedBehavior: gdEventsBasedBehavior, - codeNamespacePrefix: string -) => { - return codeNamespacePrefix + '__' + mangleName(eventsBasedBehavior.getName()); -}; - -/** Generate the namespace for an object function. */ -const getObjectFunctionCodeNamespace = ( - eventsBasedObject: gdEventsBasedObject, - codeNamespacePrefix: string -) => { - return codeNamespacePrefix + '__' + mangleName(eventsBasedObject.getName()); -}; - /** * Load all events functions of a project in extensions */ @@ -160,18 +113,15 @@ const loadProjectEventsFunctionsExtension = ( const getExtensionIncludeFiles = ( project: gdProject, eventsFunctionsExtension: gdEventsFunctionsExtension, - options: Options, - codeNamespacePrefix: string + options: Options ): Array => { return mapFor(0, eventsFunctionsExtension.getEventsFunctionsCount(), i => { const eventsFunction = eventsFunctionsExtension.getEventsFunctionAt(i); - const codeNamespace = getFreeFunctionCodeNamespace( - eventsFunction, - codeNamespacePrefix + const functionName = gd.MetadataDeclarationHelper.getFreeFunctionCodeName( + eventsFunctionsExtension, + eventsFunction ); - // TODO Use getFreeFunctionCodeName. - const functionName = codeNamespace + '.func'; return options.eventsFunctionCodeWriter.getIncludeFileFor(functionName); }).filter(Boolean); @@ -191,14 +141,14 @@ const generateEventsFunctionExtension = ( eventsFunctionsExtension ); - const codeNamespacePrefix = - 'gdjs.evtsExt__' + mangleName(eventsFunctionsExtension.getName()); + const codeNamespacePrefix = gd.MetadataDeclarationHelper.getExtensionCodeNamespacePrefix( + eventsFunctionsExtension + ); const extensionIncludeFiles = getExtensionIncludeFiles( project, eventsFunctionsExtension, - options, - codeNamespacePrefix + options ); const codeGenerationContext = { codeNamespacePrefix, @@ -276,14 +226,14 @@ const generateEventsFunctionExtensionMetadata = ( eventsFunctionsExtension ); - const codeNamespacePrefix = - 'gdjs.evtsExt__' + mangleName(eventsFunctionsExtension.getName()); + const codeNamespacePrefix = gd.MetadataDeclarationHelper.getExtensionCodeNamespacePrefix( + eventsFunctionsExtension + ); const extensionIncludeFiles = getExtensionIncludeFiles( project, eventsFunctionsExtension, - options, - codeNamespacePrefix + options ); const codeGenerationContext = { codeNamespacePrefix, @@ -369,7 +319,7 @@ const generateFreeFunction = ( const eventsFunctionsExtensionCodeGenerator = new gd.EventsFunctionsExtensionCodeGenerator( project ); - const codeNamespace = getFreeFunctionCodeNamespace( + const codeNamespace = gd.MetadataDeclarationHelper.getFreeFunctionCodeNamespace( eventsFunction, codeGenerationContext.codeNamespacePrefix ); @@ -398,8 +348,10 @@ const generateFreeFunction = ( eventsFunctionsExtensionCodeGenerator.delete(); metadataDeclarationHelper.delete(); - // TODO Implement an helper function for free function names. - const functionName = codeNamespace + '.func'; + const functionName = gd.MetadataDeclarationHelper.getFreeFunctionCodeName( + eventsFunctionsExtension, + eventsFunction + ); return options.eventsFunctionCodeWriter .writeFunctionCode(functionName, code) .then(() => {}); @@ -476,7 +428,7 @@ function generateBehavior( // Generate code for the behavior and its methods if (!options.skipCodeGeneration) { - const codeNamespace = getBehaviorFunctionCodeNamespace( + const codeNamespace = gd.MetadataDeclarationHelper.getBehaviorFunctionCodeNamespace( eventsBasedBehavior, codeGenerationContext.codeNamespacePrefix ); @@ -538,7 +490,7 @@ function generateBehaviorMetadata( behaviorMethodMangledNames ); - const codeNamespace = getBehaviorFunctionCodeNamespace( + const codeNamespace = gd.MetadataDeclarationHelper.getBehaviorFunctionCodeNamespace( eventsBasedBehavior, codeGenerationContext.codeNamespacePrefix ); @@ -578,7 +530,7 @@ function generateObject( // Generate code for the object and its methods if (!options.skipCodeGeneration) { - const codeNamespace = getObjectFunctionCodeNamespace( + const codeNamespace = gd.MetadataDeclarationHelper.getObjectFunctionCodeNamespace( eventsBasedObject, codeGenerationContext.codeNamespacePrefix ); @@ -640,7 +592,7 @@ function generateObjectMetadata( objectMethodMangledNames ); - const codeNamespace = getObjectFunctionCodeNamespace( + const codeNamespace = gd.MetadataDeclarationHelper.getObjectFunctionCodeNamespace( eventsBasedObject, codeGenerationContext.codeNamespacePrefix ); diff --git a/newIDE/app/src/MainFrame/Preferences/PreferencesContext.js b/newIDE/app/src/MainFrame/Preferences/PreferencesContext.js index 43ee1fd8ae..55c320d058 100644 --- a/newIDE/app/src/MainFrame/Preferences/PreferencesContext.js +++ b/newIDE/app/src/MainFrame/Preferences/PreferencesContext.js @@ -212,6 +212,7 @@ export type PreferencesValues = {| newProjectsDefaultFolder: string, newProjectsDefaultStorageProviderName: string, useShortcutToClosePreviewWindow: boolean, + allowUsageOfUnicodeIdentifierNames: boolean, |}; /** @@ -289,6 +290,7 @@ export type Preferences = {| |}) => ?InAppTutorialUserProgress, setNewProjectsDefaultFolder: (newProjectsDefaultFolder: string) => void, setUseShortcutToClosePreviewWindow: (enabled: boolean) => void, + setAllowUsageOfUnicodeIdentifierNames: (enabled: boolean) => void, |}; export const initialPreferences = { @@ -333,6 +335,7 @@ export const initialPreferences = { newProjectsDefaultFolder: app ? findDefaultFolder(app) : '', newProjectsDefaultStorageProviderName: 'Cloud', useShortcutToClosePreviewWindow: true, + allowUsageOfUnicodeIdentifierNames: false, }, setLanguage: () => {}, setThemeName: () => {}, @@ -389,6 +392,7 @@ export const initialPreferences = { setNewProjectsDefaultFolder: () => {}, setNewProjectsDefaultStorageProviderName: () => {}, setUseShortcutToClosePreviewWindow: () => {}, + setAllowUsageOfUnicodeIdentifierNames: () => {}, }; const PreferencesContext = React.createContext(initialPreferences); diff --git a/newIDE/app/src/MainFrame/Preferences/PreferencesDialog.js b/newIDE/app/src/MainFrame/Preferences/PreferencesDialog.js index 1f6ffba133..bc50f1299c 100644 --- a/newIDE/app/src/MainFrame/Preferences/PreferencesDialog.js +++ b/newIDE/app/src/MainFrame/Preferences/PreferencesDialog.js @@ -69,6 +69,7 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => { setUse3DEditor, setNewProjectsDefaultFolder, setUseShortcutToClosePreviewWindow, + setAllowUsageOfUnicodeIdentifierNames, } = React.useContext(PreferencesContext); const initialUse3DEditor = React.useRef(values.use3DEditor); @@ -354,6 +355,19 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => { } /> + + setAllowUsageOfUnicodeIdentifierNames(check) + } + toggled={values.allowUsageOfUnicodeIdentifierNames} + labelPosition="right" + label={ + + Allow unicode characters (non English languages and emojis) in + object, behavior and other names (experimental) + + } + /> setUse3DEditor(check)} toggled={values.use3DEditor} diff --git a/newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js b/newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js index 140ab5b04c..b82b295d02 100644 --- a/newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js +++ b/newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js @@ -61,6 +61,12 @@ export const loadPreferencesFromLocalStorage = (): ?PreferencesValues => { values.themeName = 'Blue Dark'; } + // Synchronize the global state(s). + const gd: libGDevelop = global.gd; + gd.Project.allowUsageOfUnicodeIdentifierNames( + values.allowUsageOfUnicodeIdentifierNames + ); + return values; } catch (e) { return null; @@ -159,6 +165,9 @@ export default class PreferencesProvider extends React.Component { setUseShortcutToClosePreviewWindow: this._setUseShortcutToClosePreviewWindow.bind( this ), + setAllowUsageOfUnicodeIdentifierNames: this._setAllowUsageOfUnicodeIdentifierNames.bind( + this + ), }; componentDidMount() { @@ -819,6 +828,21 @@ export default class PreferencesProvider extends React.Component { ); } + _setAllowUsageOfUnicodeIdentifierNames(enable: boolean) { + const gd: libGDevelop = global.gd; + gd.Project.allowUsageOfUnicodeIdentifierNames(enable); + + this.setState( + state => ({ + values: { + ...state.values, + allowUsageOfUnicodeIdentifierNames: enable, + }, + }), + () => this._persistValuesToLocalStorage(this.state) + ); + } + render() { return ( diff --git a/newIDE/app/src/Utils/CaseSensitiveSlug.js b/newIDE/app/src/Utils/CaseSensitiveSlug.js deleted file mode 100644 index 3f5c6cb287..0000000000 --- a/newIDE/app/src/Utils/CaseSensitiveSlug.js +++ /dev/null @@ -1,34 +0,0 @@ -// @flow - -/* - * Case Sensitive Slug method - * Inspired by Aaron Stannard module (https://github.com/Aaronontheweb/node-slugs) - */ -export const caseSensitiveSlug = ( - incString: string, - separator?: string, - preserved?: Array -): string => { - var p = ['.', '=', '-']; - var s = '-'; - - if (typeof preserved != 'undefined') { - p = preserved; - } - - if (typeof separator != 'undefined') { - s = separator; - } - - return incString - .replace(/รผ/g, 'ue') - .replace(/รค/g, 'ae') - .replace(/รถ/g, 'oe') - .replace(/รŸ/g, 'ss') - .replace(new RegExp('[' + p.join('') + ']', 'g'), ' ') // replace preserved characters with spaces - .replace(/-{2,}/g, ' ') // remove duplicate spaces - .replace(/^\s\s*/, '') - .replace(/\s\s*$/, '') // trim both sides of string - .replace(/[^\w ]/gi, '') // replaces all non-alphanumeric with empty string - .replace(/[ ]/gi, s); // Convert spaces to dashes -};