Add support for unicode names (any character, including emojis) for object/group/behavior/extension/function names (#5564)

* In other words: this allows to use emojis, accented characters, CJK languages and any word from any language to name anything in GDevelop. This should be very useful for non english speaking users.
* This must be activated in the preferences of GDevelop. This will be activated by default once this is proven to work without bugs.
This commit is contained in:
Florian Rival
2023-08-14 21:46:10 +02:00
committed by GitHub
parent 68968b603a
commit 4a4bf6d761
19 changed files with 413 additions and 246 deletions
@@ -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 {
+5 -91
View File
@@ -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];
@@ -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
+41 -9
View File
@@ -15,6 +15,7 @@
#include <vector>
#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, '_');
}
}
+18 -2
View File
@@ -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
+95 -10
View File
@@ -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<gd::IdentifierNode &>(*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<gd::IdentifierNode &>(*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<gd::FunctionCallNode &>(*node);
REQUIRE(functionNode.functionName == "WhateverFunction");
REQUIRE(functionNode.objectName == "WhateverObject");
REQUIRE(functionNode.behaviorName == "WhateverBehavior");
REQUIRE(functionNode.parameters.size() == 3);
auto &numberNode =
dynamic_cast<gd::NumberNode &>(*functionNode.parameters[0]);
auto &textNode =
dynamic_cast<gd::TextNode &>(*functionNode.parameters[1]);
auto &identifierNode =
dynamic_cast<gd::IdentifierNode &>(*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<gd::FunctionCallNode &>(*node);
REQUIRE(functionNode.functionName == "🔔");
REQUIRE(functionNode.objectName == "🧸");
REQUIRE(functionNode.behaviorName == "🗣️");
REQUIRE(functionNode.parameters.size() == 3);
auto &numberNode =
dynamic_cast<gd::NumberNode &>(*functionNode.parameters[0]);
auto &textNode =
dynamic_cast<gd::TextNode &>(*functionNode.parameters[1]);
auto &identifierNode =
dynamic_cast<gd::IdentifierNode &>(*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<gd::IdentifierNode &>(*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<gd::IdentifierNode &>(*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);
@@ -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());
}
@@ -66,10 +66,30 @@ public:
const gd::EventsBasedObject &eventsBasedObject,
std::map<gd::String, gd::String> &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
+21 -4
View File
@@ -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);
+6
View File
@@ -522,6 +522,8 @@ typedef ExtensionAndMetadata<ExpressionMetadata> 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<ExpressionMetadata> 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
+21 -1
View File
@@ -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)
@@ -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();
@@ -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;
+2
View File
@@ -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;
@@ -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<string>,
|};
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<string> => {
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
);
@@ -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<Preferences>(initialPreferences);
@@ -69,6 +69,7 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
setUse3DEditor,
setNewProjectsDefaultFolder,
setUseShortcutToClosePreviewWindow,
setAllowUsageOfUnicodeIdentifierNames,
} = React.useContext(PreferencesContext);
const initialUse3DEditor = React.useRef<boolean>(values.use3DEditor);
@@ -354,6 +355,19 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
</Trans>
}
/>
<Toggle
onToggle={(e, check) =>
setAllowUsageOfUnicodeIdentifierNames(check)
}
toggled={values.allowUsageOfUnicodeIdentifierNames}
labelPosition="right"
label={
<Trans>
Allow unicode characters (non English languages and emojis) in
object, behavior and other names (experimental)
</Trans>
}
/>
<Toggle
onToggle={(e, check) => setUse3DEditor(check)}
toggled={values.use3DEditor}
@@ -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<Props, State> {
setUseShortcutToClosePreviewWindow: this._setUseShortcutToClosePreviewWindow.bind(
this
),
setAllowUsageOfUnicodeIdentifierNames: this._setAllowUsageOfUnicodeIdentifierNames.bind(
this
),
};
componentDidMount() {
@@ -819,6 +828,21 @@ export default class PreferencesProvider extends React.Component<Props, State> {
);
}
_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 (
<PreferencesContext.Provider value={this.state}>
-34
View File
@@ -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>
): 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
};