refactor: Splitting ANSI into codes and texts, instead of converting straight to HTML

This commit is contained in:
Chris Cameron
2020-02-25 19:05:40 -05:00
parent a82645e902
commit 3f62e7e929
7 changed files with 137 additions and 98 deletions

View File

@@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Added public access to GetValues enumeration extension - Added public access to GetValues enumeration extension
- Added extensions for getting JsonReader values as long or ulong - Added extensions for getting JsonReader values as long or ulong
- Added DateTimeUtils methods for creating DateTimes from epoch seconds or milliseconds - Added DateTimeUtils methods for creating DateTimes from epoch seconds or milliseconds
- Added AnsiToHtml converter for visualizing ANSI logs - Added utils for splitting ANSI into spans for conversion to XAML, HTML, etc
### Changed ### Changed
- Fixed exception trying to get DHCP status of network interfaces on Linux - Fixed exception trying to get DHCP status of network interfaces on Linux

View File

@@ -0,0 +1,27 @@
using System.Linq;
using NUnit.Framework;
namespace ICD.Common.Utils.Tests
{
[TestFixture]
public sealed class AnsiUtilsTest
{
[Test]
public void ToSpansTest()
{
string ansi = "\x1b[30mblack\x1b[37mwhite\x1b[0mdefault";
AnsiSpan[] spans = AnsiUtils.ToSpans(ansi).ToArray();
Assert.AreEqual(3, spans.Length);
Assert.AreEqual("black", spans[0].Text);
Assert.AreEqual("30", spans[0].Code);
Assert.AreEqual("white", spans[1].Text);
Assert.AreEqual("37", spans[1].Code);
Assert.AreEqual("default", spans[2].Text);
Assert.AreEqual("0", spans[2].Code);
}
}
}

View File

@@ -1,22 +0,0 @@
using ICD.Common.Utils.Converters;
using NUnit.Framework;
namespace ICD.Common.Utils.Tests.Converters
{
[TestFixture]
public sealed class AnsiToHtmlTest
{
[TestCase("black\x1b[37mwhite",
@"black<span style=""color:#BBBBBB"">white</span>")]
[TestCase("black\x1b[0mblack",
@"blackblack")]
[TestCase("black\x1b[37mwhite\x1b[0m",
@"black<span style=""color:#BBBBBB"">white</span>")]
[TestCase("\x1b[30mblack\x1b[37mwhite",
@"<span style=""color:#000000"">black<span style=""color:#BBBBBB"">white</span></span>")]
public void ConvertTest(string ansi, string expected)
{
Assert.AreEqual(expected, AnsiToHtml.Convert(ansi));
}
}
}

View File

@@ -1,4 +1,7 @@
namespace ICD.Common.Utils using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace ICD.Common.Utils
{ {
public static class AnsiUtils public static class AnsiUtils
{ {
@@ -13,6 +16,42 @@
public const string CODE_RESET = "\x1b[0m"; public const string CODE_RESET = "\x1b[0m";
/// <summary>
/// Matches ANSI escape codes, e.g. \x1b[31m and \x1b[30;1m
/// </summary>
private const string ANSI_PATTERN = "(?'match'\x01b\\[(?'code'[\\d;]+)m)";
/// <summary>
/// Matches ANSI escape codes to HTML styles.
/// Color values are taken from PuTTY.
/// </summary>
private static readonly Dictionary<string, string> s_PuttyColors =
new Dictionary<string, string>
{
{"30", "000000"}, // Black
{"31", "BB0000"}, // Red
{"32", "00BB00"}, // Green
{"33", "BBBB00"}, // Yellow
{"34", "0000BB"}, // Blue
{"35", "BB00BB"}, // Magenta
{"36", "00BBBB"}, // Cyan
{"37", "BBBBBB"}, // White
{"30;1", "555555"}, // Bright Black
{"31;1", "FF5555"}, // Bright Red
{"32;1", "55FF55"}, // Bright Green
{"33;1", "FFFF55"}, // Bright Yellow
{"34;1", "5555FF"}, // Bright Blue
{"35;1", "FF55FF"}, // Bright Magenta
{"36;1", "55FFFF"}, // Bright Cyan
{"37;1", "FFFFFF"}, // Bright White
};
/// <summary>
/// Gets the color map matching PuTTY.
/// </summary>
public static IDictionary<string, string> PuttyColors { get { return s_PuttyColors; } }
/// <summary> /// <summary>
/// Constructor. /// Constructor.
/// </summary> /// </summary>
@@ -37,5 +76,73 @@
return string.Format("{0}{1}{2}", code, data, CODE_RESET); return string.Format("{0}{1}{2}", code, data, CODE_RESET);
} }
/// <summary>
/// Splits the given ANSI string into spans.
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public static IEnumerable<AnsiSpan> ToSpans(string data)
{
if (string.IsNullOrEmpty(data))
yield break;
Regex regex = new Regex(ANSI_PATTERN);
Match match = regex.Match(data);
// No matches
if (!match.Success)
yield return new AnsiSpan {Text = data};
// Find the spans
while (match.Success)
{
// Get the code
string code = match.Groups["code"].Value;
// Get the text
Match next = match.NextMatch();
int startIndex = match.Index + match.Length;
int endIndex = next.Success ? next.Index : data.Length;
string text = data.Substring(startIndex, endIndex - startIndex);
// Build the span
if (text.Length > 0)
yield return new AnsiSpan { Code = code, Text = text };
// Loop
match = next;
}
}
/// <summary>
/// Removes the bright suffix from the code if present, otherwise appends a bright suffix.
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
public static string InvertBright(string code)
{
return code.EndsWith(";1")
? code.Substring(0, code.Length - 1)
: code + ";1";
}
}
public sealed class AnsiSpan
{
public string Code { get; set; }
public string Text { get; set; }
/// <summary>
/// Gets the color value for the code.
/// </summary>
/// <param name="colors"></param>
/// <param name="invertBright"></param>
/// <returns></returns>
public string GetColor(IDictionary<string, string> colors, bool invertBright)
{
string code = invertBright ? AnsiUtils.InvertBright(Code) : Code;
return colors[code];
}
} }
} }

View File

@@ -1,73 +0,0 @@
using System.Collections.Generic;
namespace ICD.Common.Utils.Converters
{
public static class AnsiToHtml
{
/// <summary>
/// Matches ANSI escape codes, e.g. \x1b[31m and \x1b[30;1m
/// </summary>
private const string ANSI_PATTERN = "(?'match'\x01b\\[(?'code'[\\d;]+)m)";
private const string CODE_RESET = "0";
/// <summary>
/// Matches ANSI escape codes to HTML styles.
/// Color values are taken from PuTTY.
/// </summary>
private static readonly Dictionary<string, string> s_Colors =
new Dictionary<string, string>
{
{"30", "color:#000000"}, // Black
{"31", "color:#BB0000"}, // Red
{"32", "color:#00BB00"}, // Green
{"33", "color:#BBBB00"}, // Yellow
{"34", "color:#0000BB"}, // Blue
{"35", "color:#BB00BB"}, // Magenta
{"36", "color:#00BBBB"}, // Cyan
{"37", "color:#BBBBBB"}, // White
{"30;1", "color:#555555"}, // Bright Black
{"31;1", "color:#FF5555"}, // Bright Red
{"32;1", "color:#55FF55"}, // Bright Green
{"33;1", "color:#FFFF55"}, // Bright Yellow
{"34;1", "color:#5555FF"}, // Bright Blue
{"35;1", "color:#FF55FF"}, // Bright Magenta
{"36;1", "color:#55FFFF"}, // Bright Cyan
{"37;1", "color:#FFFFFF"}, // Bright White
};
/// <summary>
/// Converts the input ansi string into html with color attributes.
/// </summary>
/// <param name="ansi"></param>
/// <returns></returns>
public static string Convert(string ansi)
{
int depth = 0;
// Hack - Append a reset to close any open spans
ansi += AnsiUtils.CODE_RESET;
return RegexUtils.ReplaceGroup(ansi, ANSI_PATTERN, "match", match =>
{
string code = match.Groups["code"].Value;
// Reset code - close all of the open spans.
if (code == CODE_RESET)
{
string output = StringUtils.Repeat("</span>", depth);
depth = 0;
return output;
}
string style;
if (!s_Colors.TryGetValue(code, out style))
return match.Value;
depth++;
return string.Format("<span style=\"{0}\">", style);
});
}
}
}

View File

@@ -87,7 +87,6 @@
<Compile Include="Comparers\PredicateComparer.cs" /> <Compile Include="Comparers\PredicateComparer.cs" />
<Compile Include="Comparers\SequenceComparer.cs" /> <Compile Include="Comparers\SequenceComparer.cs" />
<Compile Include="eConsoleColor.cs" /> <Compile Include="eConsoleColor.cs" />
<Compile Include="Converters\AnsiToHtml.cs" />
<Compile Include="DateTimeUtils.cs" /> <Compile Include="DateTimeUtils.cs" />
<Compile Include="Email\EmailClient.cs" /> <Compile Include="Email\EmailClient.cs" />
<Compile Include="Email\eMailErrorCode.cs" /> <Compile Include="Email\eMailErrorCode.cs" />

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;