Jelajahi Sumber

REXM: Support examples data `validation` and **report generation**

Ray 1 bulan lalu
induk
melakukan
59546eb54a
1 mengubah file dengan 150 tambahan dan 89 penghapusan
  1. 150 89
      tools/rexm/rexm.c

+ 150 - 89
tools/rexm/rexm.c

@@ -147,8 +147,9 @@ static void UnloadExamplesData(rlExampleInfo *exInfo);
 static char **LoadTextLines(const char *text, int *count);
 static char **LoadTextLines(const char *text, int *count);
 static void UnloadTextLines(char **text);
 static void UnloadTextLines(char **text);
 
 
-// Get example info from file header
-static rlExampleInfo *GetExampleInfo(const char *exFileName);
+// Load example info from file header
+static rlExampleInfo *LoadExampleInfo(const char *exFileName);
+static void UnloadExampleInfo(rlExampleInfo *exInfo);
 
 
 // raylib example line info parser
 // raylib example line info parser
 // Parses following line format: core/core_basic_window;⭐️☆☆☆;1.0;1.0;"Ray"/@raysan5
 // Parses following line format: core/core_basic_window;⭐️☆☆☆;1.0;1.0;"Ray"/@raysan5
@@ -441,7 +442,7 @@ int main(int argc, char *argv[])
 
 
                 // Get required example info from example file header (if provided)
                 // Get required example info from example file header (if provided)
                 // NOTE: If no example info is provided (other than category/name), just using some default values
                 // NOTE: If no example info is provided (other than category/name), just using some default values
-                rlExampleInfo *exInfo = GetExampleInfo(TextFormat("%s/%s/%s.c", exBasePath, exCategory, exName));
+                rlExampleInfo *exInfo = LoadExampleInfo(TextFormat("%s/%s/%s.c", exBasePath, exCategory, exName));
                 
                 
                 // Get example difficulty stars
                 // Get example difficulty stars
                 char starsText[16] = { 0 };
                 char starsText[16] = { 0 };
@@ -470,7 +471,7 @@ int main(int argc, char *argv[])
                     memcpy(exCollectionListUpdated + categoryIndex + textWritenSize, exCollectionList + categoryIndex, strlen(exCollectionList) - categoryIndex);
                     memcpy(exCollectionListUpdated + categoryIndex + textWritenSize, exCollectionList + categoryIndex, strlen(exCollectionList) - categoryIndex);
                 }
                 }
 
 
-                RL_FREE(exInfo);
+                UnloadExampleInfo(exInfo);
                 
                 
                 SaveFileText(exCollectionFilePath, exCollectionListUpdated);
                 SaveFileText(exCollectionFilePath, exCollectionListUpdated);
                 RL_FREE(exCollectionListUpdated);
                 RL_FREE(exCollectionListUpdated);
@@ -721,7 +722,7 @@ int main(int argc, char *argv[])
 
 
             // TODO: Validate: Duplicate entries in collection list?
             // TODO: Validate: Duplicate entries in collection list?
 
 
-            // Get status information for all examples, using "status" field in the struct
+            // Set status information for all examples, using "status" field in the struct
             for (int i = 0; i < exCollectionCount; i++)
             for (int i = 0; i < exCollectionCount; i++)
             {
             {
                 rlExampleInfo *exInfo = &exCollection[i];
                 rlExampleInfo *exInfo = &exCollection[i];
@@ -736,7 +737,8 @@ int main(int argc, char *argv[])
                 // Validate: example screenshot is not the template default one
                 // Validate: example screenshot is not the template default one
                 Image imScreenshot = LoadImage(TextFormat("%s/%s/%s.png", exBasePath, exInfo->category, exInfo->name));
                 Image imScreenshot = LoadImage(TextFormat("%s/%s/%s.png", exBasePath, exInfo->category, exInfo->name));
                 Image imTemplate = LoadImage(TextFormat("%s/examples_template.png", exBasePath));
                 Image imTemplate = LoadImage(TextFormat("%s/examples_template.png", exBasePath));
-                if (memcmp(imScreenshot.data, imTemplate.data, GetPixelDataSize(imScreenshot.width, imScreenshot.height, imScreenshot.format)) != 0) exInfo->status |= VALID_INVALID_PNG;
+                if (memcmp(imScreenshot.data, imTemplate.data, GetPixelDataSize(imScreenshot.width, imScreenshot.height, imScreenshot.format)) == 0) 
+                    exInfo->status |= VALID_INVALID_PNG;
                 UnloadImage(imTemplate);
                 UnloadImage(imTemplate);
                 UnloadImage(imScreenshot);
                 UnloadImage(imScreenshot);
 
 
@@ -747,13 +749,13 @@ int main(int argc, char *argv[])
                 if (FileTextFind(TextFormat("%s/Makefile.Web", exBasePath), exInfo->name) == -1) exInfo->status |= VALID_NOT_IN_MAKEFILE_WEB;
                 if (FileTextFind(TextFormat("%s/Makefile.Web", exBasePath), exInfo->name) == -1) exInfo->status |= VALID_NOT_IN_MAKEFILE_WEB;
 
 
                 // Validate: raylib/examples/README.md                                  -> Example listed?
                 // Validate: raylib/examples/README.md                                  -> Example listed?
-                if (FileTextFind(TextFormat("%s/README.md", exBasePath), exInfo->name) == -1) exInfo->status |= VALID_NOT_IN_JS;
+                if (FileTextFind(TextFormat("%s/README.md", exBasePath), exInfo->name) == -1) exInfo->status |= VALID_NOT_IN_README;
                 
                 
                 // Validate: raylib.com/common/examples.js                              -> Example listed?
                 // Validate: raylib.com/common/examples.js                              -> Example listed?
-                if (FileTextFind(TextFormat("%s/common/examples.js", exWebPath), exInfo->name) == -1) exInfo->status |= VALID_NOT_IN_README;
+                if (FileTextFind(TextFormat("%s/../common/examples.js", exWebPath), exInfo->name + TextFindIndex(exInfo->name, "_") + 1) == -1) exInfo->status |= VALID_NOT_IN_JS;
 
 
                 // Validate: raylib/projects/VS2022/examples/<category>_example_name.vcxproj -> File exists?
                 // Validate: raylib/projects/VS2022/examples/<category>_example_name.vcxproj -> File exists?
-                if (!FileExists(TextFormat("%s/../projects/VS2022/examples/%s.png", exBasePath, exInfo->name))) exInfo->status |= VALID_MISSING_VCXPROJ;
+                if (!FileExists(TextFormat("%s/../projects/VS2022/examples/%s.vcxproj", exBasePath, exInfo->name))) exInfo->status |= VALID_MISSING_VCXPROJ;
 
 
                 // Validate: raylib/projects/VS2022/raylib.sln                          -> Example listed?
                 // Validate: raylib/projects/VS2022/raylib.sln                          -> Example listed?
                 if (FileTextFind(TextFormat("%s/../projects/VS2022/raylib.sln", exBasePath), exInfo->name) == -1) exInfo->status |= VALID_NOT_IN_VCXSOL;
                 if (FileTextFind(TextFormat("%s/../projects/VS2022/raylib.sln", exBasePath), exInfo->name) == -1) exInfo->status |= VALID_NOT_IN_VCXSOL;
@@ -774,13 +776,22 @@ int main(int argc, char *argv[])
                             for (int v = 0; v < 3; v++)
                             for (int v = 0; v < 3; v++)
                             {
                             {
                                 char *resPathUpdated = TextReplace(resPaths[r], "glsl%i", TextFormat("glsl%i", glslVer[v]));
                                 char *resPathUpdated = TextReplace(resPaths[r], "glsl%i", TextFormat("glsl%i", glslVer[v]));
-                                if (!FileExists(TextFormat("%s/%s/%s", exBasePath, exInfo->category, resPathUpdated))) exInfo->status |= VALID_MISSING_RESOURCES;
+                                if (!FileExists(TextFormat("%s/%s/%s", exBasePath, exInfo->category, resPathUpdated)))
+                                {
+                                    exInfo->status |= VALID_MISSING_RESOURCES;
+                                    // Logging missing resources for convenience
+                                    LOG("WARNING: [%s] Missing resource: %s\n", exInfo->name, resPathUpdated);
+                                }
                                 RL_FREE(resPathUpdated);
                                 RL_FREE(resPathUpdated);
                             }
                             }
                         }
                         }
                         else
                         else
                         {
                         {
-                            if (!FileExists(TextFormat("%s/%s/%s", exBasePath, exInfo->category, resPaths[r]))) exInfo->status |= VALID_MISSING_RESOURCES;
+                            if (!FileExists(TextFormat("%s/%s/%s", exBasePath, exInfo->category, resPaths[r])))
+                            {
+                                exInfo->status |= VALID_MISSING_RESOURCES;
+                                LOG("WARNING: [%s] Missing resource: %s\n", exInfo->name, resPaths[r]);
+                            }
                         }
                         }
                     }
                     }
                 }
                 }
@@ -790,21 +801,22 @@ int main(int argc, char *argv[])
                 // Validate: raylib.com/examples/<category>/<category>_example_name.data -> File exists?
                 // Validate: raylib.com/examples/<category>/<category>_example_name.data -> File exists?
                 // Validate: raylib.com/examples/<category>/<category>_example_name.wasm -> File exists?
                 // Validate: raylib.com/examples/<category>/<category>_example_name.wasm -> File exists?
                 // Validate: raylib.com/examples/<category>/<category>_example_name.js   -> File exists?
                 // Validate: raylib.com/examples/<category>/<category>_example_name.js   -> File exists?
-                if ((!FileExists(TextFormat("%s/examples/%s/%s.html", exWebPath, exInfo->category, exInfo->name))) ||
-                    ((exInfo->resCount > 0) && !FileExists(TextFormat("%s/examples/%s/%s.data", exWebPath, exInfo->category, exInfo->name))) ||
-                    (!FileExists(TextFormat("%s/examples/%s/%s.wasm", exWebPath, exInfo->category, exInfo->name))) ||
-                    (!FileExists(TextFormat("%s/examples/%s/%s.js", exWebPath, exInfo->category, exInfo->name)))) exInfo->status |= VALID_MISSING_WEB_OUTPUT;
+                if (!FileExists(TextFormat("%s/%s/%s.html", exWebPath, exInfo->category, exInfo->name)) ||
+                    !FileExists(TextFormat("%s/%s/%s.wasm", exWebPath, exInfo->category, exInfo->name)) ||
+                    !FileExists(TextFormat("%s/%s/%s.js", exWebPath, exInfo->category, exInfo->name)) ||
+                    ((exInfo->resCount > 0) && !FileExists(TextFormat("%s/%s/%s.data", exWebPath, exInfo->category, exInfo->name))))
+                     exInfo->status |= VALID_MISSING_WEB_OUTPUT;
 
 
                 // NOTE: Additional validation elements
                 // NOTE: Additional validation elements
                 // Validate: Example naming conventions: <category>/<category>_example_name, valid category
                 // Validate: Example naming conventions: <category>/<category>_example_name, valid category
                 if ((TextFindIndex(exInfo->name, exInfo->category) == -1) || 
                 if ((TextFindIndex(exInfo->name, exInfo->category) == -1) || 
-                    (!TextIsEqual(exInfo->category, "core") || !TextIsEqual(exInfo->category, "shapes") || 
-                     !TextIsEqual(exInfo->category, "textures") || !TextIsEqual(exInfo->category, "text") || 
-                     !TextIsEqual(exInfo->category, "models") || !TextIsEqual(exInfo->category, "shaders") || 
-                     !TextIsEqual(exInfo->category, "audio") || !TextIsEqual(exInfo->category, "others"))) exInfo->status |= VALID_INVALID_CATEGORY;
+                    (!TextIsEqual(exInfo->category, "core") && !TextIsEqual(exInfo->category, "shapes") && 
+                     !TextIsEqual(exInfo->category, "textures") && !TextIsEqual(exInfo->category, "text") && 
+                     !TextIsEqual(exInfo->category, "models") && !TextIsEqual(exInfo->category, "shaders") && 
+                     !TextIsEqual(exInfo->category, "audio") && !TextIsEqual(exInfo->category, "others"))) exInfo->status |= VALID_INVALID_CATEGORY;
 
 
                 // Validate: Example info (stars, author, github) missmatches with example header content
                 // Validate: Example info (stars, author, github) missmatches with example header content
-                rlExampleInfo *exInfoHeader = GetExampleInfo(TextFormat("%s/%s/%s.c", exBasePath, exInfo->category, exInfo->name));
+                rlExampleInfo *exInfoHeader = LoadExampleInfo(TextFormat("%s/%s/%s.c", exBasePath, exInfo->category, exInfo->name));
 
 
                 if ((strcmp(exInfo->name, exInfoHeader->name) != 0) ||     // NOTE: Get it from example, not file
                 if ((strcmp(exInfo->name, exInfoHeader->name) != 0) ||     // NOTE: Get it from example, not file
                     (strcmp(exInfo->category, exInfoHeader->category) != 0) ||
                     (strcmp(exInfo->category, exInfoHeader->category) != 0) ||
@@ -814,62 +826,103 @@ int main(int argc, char *argv[])
                     (exInfo->verCreated != exInfoHeader->verCreated) ||
                     (exInfo->verCreated != exInfoHeader->verCreated) ||
                     (exInfo->verUpdated != exInfoHeader->verUpdated)) exInfo->status |= VALID_INCONSISTENT_INFO;
                     (exInfo->verUpdated != exInfoHeader->verUpdated)) exInfo->status |= VALID_INCONSISTENT_INFO;
 
 
-                RL_FREE(exInfoHeader);
+                UnloadExampleInfo(exInfoHeader);
+            }
 
 
-                // TODO: Generate validation report/table with results (.md)
-                /*
-                //Status	    Description
-                //OK	        Everything found
-                //MISSING	    One or more files are missing, some can be solved automatically
-                //ERROR	        Serious inconsistencies
-
-                Columns:
-                [C]     VALID_MISSING_C             // Missing .c source file
-                [PNG]   VALID_MISSING_PNG           // Missing screenshot .png
-                [WPNG]  VALID_INVALID_PNG           // Invalid png screenshot (using template one)
-                [RES]   VALID_MISSING_RESOURCES     // Missing resources listed in the code
-                [VCX]   VALID_MISSING_VCXPROJ       // Missing Visual Studio .vcxproj file
-                [SOL]   VALID_NOT_IN_VCXSOL         // Project not included in solution file
-                [MK]    VALID_NOT_IN_MAKEFILE       // Not listed in Makefile
-                [MKWEB] VALID_NOT_IN_MAKEFILE_WEB   // Not listed in Makefile.Web
-                [RDME]  VALID_NOT_IN_README         // Not listed in README.md
-                [JS]    VALID_NOT_IN_JS             // Not listed in examples.js
-                [WOUT]  VALID_MISSING_WEB_OUTPUT    // Missing .html/.data/.wasm/.js
-                [INFO]  VALID_INCONSISTENT_INFO     // Inconsistent info between collection and example header (stars, author...)
-                [CAT]   VALID_INVALID_CATEGORY      // Not a recognized category
-
-                [STATUS]  [CATEGORY]   [NAME]          [C] [PNG] [WPNG] [RES] [VCX] [SOL] [MK] [MKWEB] [RDME] [JS] [WOUT] [INFO] [CAT]
-                -----------------------------------------------------------------------------------------------------------------------
-                OK          core      basic_window      ✔   ✔     ✔     ✔    ✔    ✔    ✔     ✔      ✔    ✔    ✔      ✔    ✔
-                MISSING     shapes    colorful_lines    ✘   ✔     ✘     ✔    ✘    ✔    ✔     ✘      ✔    ✔    ✔      ✔    ✔
-                ERROR       text      broken_shader     ✘   ✘     ✘     ✘    ✘    ✘    ✘     ✘      ✔    ✘    ✔      ✔    ✔
-                */
+            // Generate validation report/table with results (.md)
+            /*
+            Columns:
+            [C]     VALID_MISSING_C             // Missing .c source file
+            [PNG]   VALID_MISSING_PNG           // Missing screenshot .png
+            [WPNG]  VALID_INVALID_PNG           // Invalid png screenshot (using template one)
+            [RES]   VALID_MISSING_RESOURCES     // Missing resources listed in the code
+            [VCX]   VALID_MISSING_VCXPROJ       // Missing Visual Studio .vcxproj file
+            [SOL]   VALID_NOT_IN_VCXSOL         // Project not included in solution file
+            [MK]    VALID_NOT_IN_MAKEFILE       // Not listed in Makefile
+            [MKWEB] VALID_NOT_IN_MAKEFILE_WEB   // Not listed in Makefile.Web
+            [RDME]  VALID_NOT_IN_README         // Not listed in README.md
+            [JS]    VALID_NOT_IN_JS             // Not listed in examples.js
+            [WOUT]  VALID_MISSING_WEB_OUTPUT    // Missing .html/.data/.wasm/.js
+            [INFO]  VALID_INCONSISTENT_INFO     // Inconsistent info between collection and example header (stars, author...)
+            [CAT]   VALID_INVALID_CATEGORY      // Not a recognized category
+
+            | [EXAMPLE NAME]               | [C] |[CAT]|[INFO]|[PNG]|[WPNG]|[RES]|[MK] |[MKWEB]|[VCX]|[SOL]|[RDME]|[JS] |[WOUT]|
+            |:-----------------------------|:---:|:---:|:----:|:---:|:----:|:---:|:---:|:-----:|:---:|:---:|:----:|:---:|:----:|
+            | core_basic_window            |  ✔ |  ✔  |  ✔  |  ✔ |  ✔  |  ✔  |  ✔ |   ✔  |  ✔  |  ✔ |  ✔  |  ✔ |   ✔  |
+            | shapes_colors_palette        |  ✘ |  ✔  |  ✘  |  ✔ |  ✘  |  ✔  |  ✔ |   ✘  |  ✔  |  ✔ |  ✔  |  ✔ |   ✔  |
+            | text_format_text             |  ✘ |  ✘  |  ✘  |  ✘ |  ✘  |  ✘  |  ✘ |   ✘  |  ✔  |  ✘ |  ✔  |  ✔ |   ✔  |
+            */
+
+            char *report = (char *)RL_CALLOC(REXM_MAX_BUFFER_SIZE, 1);
+
+            int repIndex = 0;
+            repIndex += sprintf(report + repIndex, "# EXAMPLES COLLECTION REPORT\n\n");
+            repIndex += sprintf(report + repIndex, "| **EXAMPLE NAME**                 | [C] | [CAT]| [INFO]|[PNG]|[WPNG]| [RES]| [MK] |[MKWEB]| [VCX]| [SOL]|[RDME]|[JS] | [WOUT]|\n");
+            repIndex += sprintf(report + repIndex, "|:---------------------------------|:---:|:----:|:-----:|:---:|:----:|:----:|:----:|:-----:|:----:|:----:|:----:|:---:|:-----:|\n");
+
+            for (int i = 0; i < exCollectionCount; i++)
+            {
+                repIndex += sprintf(report + repIndex, "| %-32s |  %s |  %s  |  %s  |  %s |  %s  |  %s  |  %s |   %s  |  %s  |  %s |  %s  |  %s |  %s  |\n",
+                    exCollection[i].name,
+                    (exCollection[i].status & VALID_MISSING_C)? "❌" : "✔",
+                    (exCollection[i].status & VALID_INVALID_CATEGORY)? "❌" : "✔",
+                    (exCollection[i].status & VALID_INCONSISTENT_INFO)? "❌" : "✔",
+                    (exCollection[i].status & VALID_MISSING_PNG)? "❌" : "✔",
+                    (exCollection[i].status & VALID_INVALID_PNG)? "❌" : "✔",
+                    (exCollection[i].status & VALID_MISSING_RESOURCES)? "❌" : "✔",
+                    (exCollection[i].status & VALID_NOT_IN_MAKEFILE)? "❌" : "✔",
+                    (exCollection[i].status & VALID_NOT_IN_MAKEFILE_WEB)? "❌" : "✔",
+                    (exCollection[i].status & VALID_MISSING_VCXPROJ)? "❌" : "✔",
+                    (exCollection[i].status & VALID_NOT_IN_VCXSOL)? "❌" : "✔",
+                    (exCollection[i].status & VALID_NOT_IN_README)? "❌" : "✔",
+                    (exCollection[i].status & VALID_NOT_IN_JS)? "❌" : "✔",
+                    (exCollection[i].status & VALID_MISSING_WEB_OUTPUT)? "❌" : "✔");
             }
             }
 
 
-            UnloadExamplesData(exCollection);
-            //------------------------------------------------------------------------------------------------
+            SaveFileText(TextFormat("%s/../tools/rexm/%s", exBasePath, "examples_report.md"), report);
+            RL_FREE(report);
+
+            //UnloadExamplesData(exCollection); // Done at the end, it can be required for fixing
 
 
             if (opCode == OP_UPDATE)
             if (opCode == OP_UPDATE)
             {
             {
-                // Actions to be performed to fix/review anything possible
+                // Actions to fix/review anything possible from validation results
                 //------------------------------------------------------------------------------------------------
                 //------------------------------------------------------------------------------------------------
-                
-                // TODO: Process validation results and take actions to 
-                // correct everything possible to make collection consistent
+                // Check examples "status" information
+                for (int i = 0; i < exCollectionCount; i++)
+                {
+                    rlExampleInfo *exInfo = &exCollection[i];
 
 
-                // Review: Add/Remove: raylib/projects/VS2022/examples/<category>_example_name.vcxproj
-                // Review: Add/remove: raylib/projects/VS2022/raylib.sln
+                    if (exInfo->status & VALID_MISSING_C) LOG("WARNING: [%s] Missing code file\n", exInfo->name);
+                    else
+                    {
+                        // NOTE: Some issues can not be automatically fixed, only logged
+                        //if (exInfo->status & VALID_MISSING_PNG) LOG("WARNING: [%s] Missing screenshot file\n", exInfo->name);
+                        //if (exInfo->status & VALID_INVALID_PNG) LOG("WARNING: [%s] Invalid screenshot file (using template)\n", exInfo->name);
+                        //if (exInfo->status & VALID_MISSING_RESOURCES) LOG("WARNING: [%s] Missing resources detected\n", exInfo->name);
+                        //if (exInfo->status & VALID_INCONSISTENT_INFO) LOG("WARNING: [%s] Inconsistent example header info\n", exInfo->name);
+                        //if (exInfo->status & VALID_INVALID_CATEGORY) LOG("WARNING: [%s] Invalid example category\n", exInfo->name);
+
+                        // Review: Add/Remove: raylib/projects/VS2022/examples/<category>_example_name.vcxproj
+                        // Review: Add/remove: raylib/projects/VS2022/raylib.sln
+                        // Solves: VALID_MISSING_VCXPROJ, VALID_NOT_IN_VCXSOL
+
+                        // Review: Add/Remove: raylib.com/examples/<category>/<category>_example_name.html
+                        // Review: Add/Remove: raylib.com/examples/<category>/<category>_example_name.data
+                        // Review: Add/Remove: raylib.com/examples/<category>/<category>_example_name.wasm
+                        // Review: Add/Remove: raylib.com/examples/<category>/<category>_example_name.js
+                        // Solves: VALID_MISSING_WEB_OUTPUT
+                    }
+                }
 
 
                 // Update files: Makefile, Makefile.Web, README.md, examples.js
                 // Update files: Makefile, Makefile.Web, README.md, examples.js
-                UpdateRequiredFiles();
-
-                // Review examples
-                // Review: Add/Remove: raylib.com/examples/<category>/<category>_example_name.html
-                // Review: Add/Remove: raylib.com/examples/<category>/<category>_example_name.data
-                // Review: Add/Remove: raylib.com/examples/<category>/<category>_example_name.wasm
-                // Review: Add/Remove: raylib.com/examples/<category>/<category>_example_name.js
+                // Solves: VALID_NOT_IN_MAKEFILE, VALID_NOT_IN_MAKEFILE_WEB, VALID_NOT_IN_README, VALID_NOT_IN_JS
+                UpdateRequiredFiles();     
                 //------------------------------------------------------------------------------------------------
                 //------------------------------------------------------------------------------------------------
             }
             }
+
+            UnloadExamplesData(exCollection);
+            //------------------------------------------------------------------------------------------------
             
             
         } break;
         } break;
         default:    // Help
         default:    // Help
@@ -1424,7 +1477,7 @@ static void UnloadTextLines(char **lines)
 }
 }
 
 
 // Get example info from file header
 // Get example info from file header
-rlExampleInfo *GetExampleInfo(const char *exFileName)
+rlExampleInfo *LoadExampleInfo(const char *exFileName)
 {
 {
     rlExampleInfo *exInfo = (rlExampleInfo *)RL_CALLOC(1, sizeof(rlExampleInfo));
     rlExampleInfo *exInfo = (rlExampleInfo *)RL_CALLOC(1, sizeof(rlExampleInfo));
     
     
@@ -1437,6 +1490,7 @@ rlExampleInfo *GetExampleInfo(const char *exFileName)
 
 
         // Get example difficulty stars
         // Get example difficulty stars
         // NOTE: Counting the unicode char occurrences: ⭐️
         // NOTE: Counting the unicode char occurrences: ⭐️
+        // WARNING: The stars unicode in examples is not the same than in collection list!!!
         int starsIndex = TextFindIndex(exText, "★");
         int starsIndex = TextFindIndex(exText, "★");
         if (starsIndex > 0)
         if (starsIndex > 0)
         {
         {
@@ -1444,8 +1498,8 @@ rlExampleInfo *GetExampleInfo(const char *exFileName)
             while (*starPtr)
             while (*starPtr)
             {
             {
                 if (((unsigned char)starPtr[0] == 0xe2) &&
                 if (((unsigned char)starPtr[0] == 0xe2) &&
-                    ((unsigned char)starPtr[1] == 0xad) &&
-                    ((unsigned char)starPtr[2] == 0x90))
+                    ((unsigned char)starPtr[1] == 0x98) &&
+                    ((unsigned char)starPtr[2] == 0x85))
                 {
                 {
                     exInfo->stars++;
                     exInfo->stars++;
                     starPtr += 3; // Advance past multibyte character
                     starPtr += 3; // Advance past multibyte character
@@ -1469,31 +1523,32 @@ rlExampleInfo *GetExampleInfo(const char *exFileName)
         exInfo->verUpdated = TextToFloat(verUpdateText);
         exInfo->verUpdated = TextToFloat(verUpdateText);
 
 
         // Get example creator and github user
         // Get example creator and github user
-        int authorIndex = TextFindIndex(exText, "Example contributed by "); // Author = index + 23
-        int authorGitIndex = TextFindIndex(exText, "(@"); // Author GitHub user = index + 2
-        if (authorIndex > 0)
+        // NOTE: Using copyright line instead of "Example contributed by " because
+        // most examples do not contain that line --> TODO: Review examples header formating?
+        // Expected format: Copyright (c) <year_created>-<year_updated> <user_name> (@<user_github>)
+        // Alternatives:  Copyright (c) <year_created> <author_name> (@<user_github>) and <contrib_name> (@<contrib_user>)
+        int copyrightIndex = TextFindIndex(exText, "Copyright (c) ");
+        int yearStartIndex = copyrightIndex + 14;
+        int yearEndIndex = TextFindIndex(exText + yearStartIndex, " ");
+        int authorStartIndex = yearStartIndex + yearEndIndex + 1;
+        int authorEndIndex = TextFindIndex(exText + authorStartIndex, " (@");
+        if (authorEndIndex != -1) // Github user also available
         {
         {
-            int authorNameLen = 0;
-            if (authorGitIndex > 0) authorNameLen = (authorGitIndex - 1) - (authorIndex + 23);
-            else
-            {
-                int authorNameEndIndex = TextFindIndex(exText + authorIndex, " and reviewed by Ramon Santamaria");
-                if (authorNameEndIndex == -1) authorNameEndIndex = TextFindIndex(exText + authorIndex, "\n");
-
-                authorNameLen = authorNameEndIndex - (authorIndex + 23);
-            }
-            strncpy(exInfo->author, exText + authorIndex + 23, authorNameLen);
+            authorEndIndex += authorStartIndex;
+            strncpy(exInfo->author, exText + authorStartIndex, authorEndIndex - authorStartIndex);
+
+            // Get GitHub user
+            int userStartIndex = authorEndIndex + 3;
+            int userEndIndex = TextFindIndex(exText + userStartIndex, ")");
+            userEndIndex += userStartIndex;
+            strncpy(exInfo->authorGitHub, exText + userStartIndex, userEndIndex - userStartIndex);
         }
         }
-        else strcpy(exInfo->author, "<author_name>");
-        if (authorGitIndex > 0)
+        else // GitHub user not found to set end, using '\n'
         {
         {
-            int authorGitEndIndex = TextFindIndex(exText + authorGitIndex, ")");
-            if (authorGitEndIndex > 0) strncpy(exInfo->authorGitHub, exText + authorGitIndex + 2, authorGitEndIndex - (authorGitIndex + 2));
+            authorEndIndex = TextFindIndex(exText + authorStartIndex, "\n");
+            authorEndIndex += authorStartIndex;
+            strncpy(exInfo->author, exText + authorStartIndex, authorEndIndex - authorStartIndex);
         }
         }
-        else strcpy(exInfo->authorGitHub, "<user_github>");
-
-        // TODO: Verify copyright line
-        // Copyright (c) <year_created>-<year_updated> <user_name> (@<user_github>)
 
 
         UnloadFileText(exText);
         UnloadFileText(exText);
     }
     }
@@ -1501,6 +1556,12 @@ rlExampleInfo *GetExampleInfo(const char *exFileName)
     return exInfo;
     return exInfo;
 }
 }
 
 
+// Unload example information
+static void UnloadExampleInfo(rlExampleInfo *exInfo)
+{
+    RL_FREE(exInfo);
+}
+
 // raylib example line info parser
 // raylib example line info parser
 // Parses following line format: core;core_basic_window;⭐️☆☆☆;1.0;1.0;"Ray";@raysan5
 // Parses following line format: core;core_basic_window;⭐️☆☆☆;1.0;1.0;"Ray";@raysan5
 static int ParseExampleInfoLine(const char *line, rlExampleInfo *entry)
 static int ParseExampleInfoLine(const char *line, rlExampleInfo *entry)
@@ -1541,7 +1602,7 @@ static int ParseExampleInfoLine(const char *line, rlExampleInfo *entry)
     if (tokens[5][0] == '"') tokens[5] += 1;
     if (tokens[5][0] == '"') tokens[5] += 1;
     if (tokens[5][strlen(tokens[5]) - 1] == '"') tokens[5][strlen(tokens[5]) - 1] = '\0';
     if (tokens[5][strlen(tokens[5]) - 1] == '"') tokens[5][strlen(tokens[5]) - 1] = '\0';
     strcpy(entry->author, tokens[5]);
     strcpy(entry->author, tokens[5]);
-    strcpy(entry->authorGitHub, tokens[6]);
+    strcpy(entry->authorGitHub, tokens[6] + 1); // Skip '@'
 
 
     return 1;
     return 1;
 }
 }