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 + { + private readonly IFileSystem _fileSystem; + + private readonly IHtmlParser _parser; + + public EmojiGeneratorCommand() + { + _fileSystem = new FileSystem(); + _parser = new HtmlParser(); + } + + public override async Task 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> 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 RenderTemplate(Path path, IReadOnlyCollection 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 @@ Always + + Always + + 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 Parse(IHtmlDocument document) + { + var rows = document + .GetNodes(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("colors"); + config.AddCommand("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 @@ +//------------------------------------------------------------------------------ +// +// 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. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Spectre.Console +{ + /// + /// Utility class for working with emojis. + /// + internal static partial class Emoji + { + private static readonly Dictionary _emojis + = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + {{~ for emoji in emojis }} { "{{ emoji.name }}", "{{ emoji.code }}" }, + {{~ end ~}} + }; + } +}