From a7b7d4e5568d93793df9e2fe13f1ebfab56727a4 Mon Sep 17 00:00:00 2001
From: Kristian Hellang <kristian@hellang.com>
Date: Tue, 15 Sep 2020 22:51:55 +0200
Subject: [PATCH] Add Generator command to generate emoji lookup table

---
 resources/scripts/Generate-Emoji.ps1          | 22 +++++
 .../Commands/EmojiGeneratorCommand.cs         | 83 +++++++++++++++++++
 resources/scripts/Generator/Generator.csproj  |  4 +
 resources/scripts/Generator/Models/Emoji.cs   | 70 ++++++++++++++++
 resources/scripts/Generator/Program.cs        |  1 +
 .../Templates/Emoji.Generated.template        | 29 +++++++
 6 files changed, 209 insertions(+)
 create mode 100644 resources/scripts/Generate-Emoji.ps1
 create mode 100644 resources/scripts/Generator/Commands/EmojiGeneratorCommand.cs
 create mode 100644 resources/scripts/Generator/Models/Emoji.cs
 create mode 100644 resources/scripts/Generator/Templates/Emoji.Generated.template

diff --git a/resources/scripts/Generate-Emoji.ps1 b/resources/scripts/Generate-Emoji.ps1
new file mode 100644
index 0000000..de99bc0
--- /dev/null
+++ b/resources/scripts/Generate-Emoji.ps1
@@ -0,0 +1,22 @@
+##########################################################
+# Script that generates the emoji lookup table.
+##########################################################
+
+$Output = Join-Path $PSScriptRoot "Temp"
+$Source = Join-Path $PSScriptRoot "/../../src/Spectre.Console"
+
+if(!(Test-Path $Output -PathType Container)) {
+    New-Item -ItemType Directory -Path $Output | Out-Null
+}
+
+# Generate the files
+Push-Location Generator
+&dotnet run -- emoji "$Output"
+if(!$?) {
+    Pop-Location
+    Throw "An error occured when generating code."
+}
+Pop-Location
+
+# Copy the files to the correct location
+Copy-Item  (Join-Path "$Output" "Emoji.Generated.cs") -Destination "$Source/Emoji.Generated.cs"
\ No newline at end of file
diff --git a/resources/scripts/Generator/Commands/EmojiGeneratorCommand.cs b/resources/scripts/Generator/Commands/EmojiGeneratorCommand.cs
new file mode 100644
index 0000000..1054e14
--- /dev/null
+++ b/resources/scripts/Generator/Commands/EmojiGeneratorCommand.cs
@@ -0,0 +1,83 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using AngleSharp.Html.Parser;
+using Generator.Models;
+using Scriban;
+using Scriban.Runtime;
+using Spectre.Cli;
+using Spectre.IO;
+using Path = Spectre.IO.Path;
+
+namespace Generator.Commands
+{
+    public sealed class EmojiGeneratorCommand : AsyncCommand<GeneratorCommandSettings>
+    {
+        private readonly IFileSystem _fileSystem;
+
+        private readonly IHtmlParser _parser;
+
+        public EmojiGeneratorCommand()
+        {
+            _fileSystem = new FileSystem();
+            _parser = new HtmlParser();
+        }
+
+        public override async Task<int> ExecuteAsync(CommandContext context, GeneratorCommandSettings settings)
+        {
+            var output = new DirectoryPath(settings.Output);
+
+            if (!_fileSystem.Directory.Exists(settings.Output))
+            {
+                _fileSystem.Directory.Create(settings.Output);
+            }
+
+            var templatePath = new FilePath("Templates/Emoji.Generated.template");
+
+            var emojis = await FetchEmojis("http://www.unicode.org/emoji/charts/emoji-list.html");
+
+            var result = await RenderTemplate(templatePath, emojis);
+
+            var outputPath = output.CombineWithFilePath(templatePath.GetFilename().ChangeExtension(".cs"));
+
+            await File.WriteAllTextAsync(outputPath.FullPath, result);
+
+            return 0;
+        }
+
+        private async Task<IReadOnlyCollection<Emoji>> FetchEmojis(string url)
+        {
+            using var http = new HttpClient();
+
+            var htmlStream = await http.GetStreamAsync(url);
+
+            var document = await _parser.ParseDocumentAsync(htmlStream);
+
+            return Emoji.Parse(document).OrderBy(x => x.Name).ToList();
+        }
+
+        private static async Task<string> RenderTemplate(Path path, IReadOnlyCollection<Emoji> emojis)
+        {
+            var text = await File.ReadAllTextAsync(path.FullPath);
+
+            var template = Template.Parse(text);
+
+            var templateContext = new TemplateContext
+            {
+                // Because of the insane amount of Emojis,
+                // we need to get rid of some secure defaults :P
+                LoopLimit = int.MaxValue,
+            };
+
+            var scriptObject = new ScriptObject();
+
+            scriptObject.Import(new { Emojis = emojis });
+
+            templateContext.PushGlobal(scriptObject);
+
+            return await template.RenderAsync(templateContext);
+        }
+    }
+}
diff --git a/resources/scripts/Generator/Generator.csproj b/resources/scripts/Generator/Generator.csproj
index 0691da3..438e44c 100644
--- a/resources/scripts/Generator/Generator.csproj
+++ b/resources/scripts/Generator/Generator.csproj
@@ -24,9 +24,13 @@
     <None Update="Templates\ColorPalette.Generated.template">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="Templates\Emoji.Generated.template">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
   </ItemGroup>
 
   <ItemGroup>
+    <PackageReference Include="AngleSharp" Version="0.14.0" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
     <PackageReference Include="Scriban" Version="2.1.3" />
     <PackageReference Include="Spectre.Cli" Version="0.36.1-preview.0.6" />
diff --git a/resources/scripts/Generator/Models/Emoji.cs b/resources/scripts/Generator/Models/Emoji.cs
new file mode 100644
index 0000000..2a7ca17
--- /dev/null
+++ b/resources/scripts/Generator/Models/Emoji.cs
@@ -0,0 +1,70 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using AngleSharp.Dom;
+using AngleSharp.Html.Dom;
+
+namespace Generator.Models
+{
+    public class Emoji
+    {
+        private static readonly string[] _headers = { "count", "code", "sample", "name" };
+
+        private Emoji(string code, string name)
+        {
+            Code = code;
+            Name = name;
+        }
+
+        public string Code { get; }
+
+        public string Name { get; }
+
+        public static IEnumerable<Emoji> Parse(IHtmlDocument document)
+        {
+            var rows = document
+                .GetNodes<IHtmlTableRowElement>(predicate: row =>
+                    row.Cells.Length >= _headers.Length && // Filter out rows that don't have enough cells.
+                    row.Cells.All(x => x.LocalName == TagNames.Td)); // We're only interested in td cells, not th.
+
+            foreach (var row in rows)
+            {
+                var dictionary = _headers
+                    .Zip(row.Cells, (header, cell) => (header, cell.TextContent.Trim()))
+                    .ToDictionary(x => x.Item1, x => x.Item2);
+
+                var code = TransformCode(dictionary["code"]);
+                var name = TransformName(dictionary["name"]);
+
+                yield return new Emoji(code, name);
+            }
+        }
+
+        private static string TransformName(string name)
+        {
+            return name.Replace(":", string.Empty)
+                .Replace(",", string.Empty)
+                .Replace(".", string.Empty)
+                .Replace("\u201c", string.Empty)
+                .Replace("\u201d", string.Empty)
+                .Replace("\u229b", string.Empty)
+                .Trim()
+                .Replace(' ', '_')
+                .ToLowerInvariant();
+        }
+
+        private static string TransformCode(string code)
+        {
+            var builder = new StringBuilder();
+
+            foreach (var part in code.Split(' '))
+            {
+                builder.Append(part.Length == 6
+                    ? part.Replace("+", "0000")
+                    : part.Replace("+", "000"));
+            }
+
+            return builder.ToString().Replace("U", "\\U");
+        }
+    }
+}
diff --git a/resources/scripts/Generator/Program.cs b/resources/scripts/Generator/Program.cs
index 30fa3f4..0bf6ece 100644
--- a/resources/scripts/Generator/Program.cs
+++ b/resources/scripts/Generator/Program.cs
@@ -11,6 +11,7 @@ namespace Generator
             app.Configure(config =>
             {
                 config.AddCommand<ColorGeneratorCommand>("colors");
+                config.AddCommand<EmojiGeneratorCommand>("emoji");
             });
 
             return app.Run(args);
diff --git a/resources/scripts/Generator/Templates/Emoji.Generated.template b/resources/scripts/Generator/Templates/Emoji.Generated.template
new file mode 100644
index 0000000..a0d61f7
--- /dev/null
+++ b/resources/scripts/Generator/Templates/Emoji.Generated.template
@@ -0,0 +1,29 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     This code was generated by a tool.
+//     Generated {{ date.now | date.to_string `%Y-%m-%d %k:%M` }}
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Spectre.Console
+{
+    /// <summary>
+    /// Utility class for working with emojis.
+    /// </summary>
+    internal static partial class Emoji
+    {
+        private static readonly Dictionary<string, string> _emojis
+            = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase)
+        {
+            {{~ for emoji in emojis }}            { "{{ emoji.name }}", "{{ emoji.code }}" },
+            {{~ end ~}}
+        };
+    }
+}