Skip to content

Commit 1f090ff

Browse files
authored
Rewrite parsing (#23)
* Rewrite the code that does the parsing to allow for new features - The code should now be able to validate the format before writing anything - The code now supports the case where the user wrote a text in brackets without EscapingTest1 * Add support for validating the format before writing anything. * Add tests to increase code coverage. * Small dead code removal and refactoring of namespace * Fixup the lexer to allow '!' after the color specifier. * Ensure we test on release bits (and capture code coverage there as well). * Bump major as the code was rewritten which could break some people.
1 parent 3f771ec commit 1f090ff

10 files changed

Lines changed: 414 additions & 93 deletions

File tree

.github/workflows/buildAndTest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- name: Build
2323
run: dotnet build --configuration Release --no-restore
2424
- name: Test
25-
run: dotnet test --no-restore --verbosity normal --collect:"XPlat Code Coverage" --results-directory:testResults
25+
run: dotnet test --configuration Release --no-restore --verbosity normal --collect:"XPlat Code Coverage" --results-directory:testResults
2626
- uses: codecov/codecov-action@v1
2727
with:
2828
file: testResults/**/coverage.cobertura.xml

pkg/nuspec.props

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
<PackageLicenseExpression>MIT</PackageLicenseExpression>
88
<PackageProjectUrl>https://github.com/AlexGhiondea/OutputColorizer</PackageProjectUrl>
99
<PackageTags>Console, Color, Print, Write, WriteLine, Output, Colorizer</PackageTags>
10-
<AssemblyVersion>1.2.2.0</AssemblyVersion>
11-
<FileVersion>1.2.2.0</FileVersion>
12-
<PackageVersion>1.2.2</PackageVersion>
13-
<Version>1.2.2</Version>
10+
<AssemblyVersion>2.0.0.0</AssemblyVersion>
11+
<FileVersion>2.0.0.0</FileVersion>
12+
<PackageVersion>2.0.0</PackageVersion>
13+
<Version>2.0.0</Version>
1414
<PackageOutputPath>..\bin\$(Configuration)\</PackageOutputPath>
1515
<Description>Write colorful messages to the console using simple to use syntax</Description>
1616
</PropertyGroup>

src/Colorizer.cs

Lines changed: 99 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
using System;
1+
using OutputColorizer.Format;
2+
using System;
23
using System.Collections.Generic;
34
using System.Text;
45

56
namespace OutputColorizer
67
{
78
public static class Colorizer
9+
810
{
911
private static IOutputWriter s_printer = new ConsoleWriter();
1012

@@ -47,109 +49,108 @@ public static void SetupWriter(IOutputWriter newWriter)
4749
private static void InternalWrite(string message, object[] args)
4850
{
4951
Stack<ConsoleColor> colors = new Stack<ConsoleColor>();
50-
Stack<int> parens = new Stack<int>();
51-
int unbalancedParens = 0;
52-
// intial state
53-
parens.Push(-1);
5452

55-
Dictionary<string, int> argMap = CreateArgumentMap(message);
56-
for (int i = 0; i < message.Length; i++)
53+
Lexer lex = new Lexer(message);
54+
Token[] tokens = lex.Tokenize();
55+
56+
CheckFormat(lex);
57+
58+
Dictionary<string, int> argMap = CreateArgumentMap(tokens, message);
59+
60+
for (int currentTokenPosition = 0; currentTokenPosition < tokens.Length; currentTokenPosition++)
5761
{
58-
char currentChar = message[i];
62+
Token currentToken = tokens[currentTokenPosition];
5963

60-
switch (currentChar)
64+
switch (currentToken.Kind)
6165
{
62-
case '\\':
66+
case TokenKind.String:
6367
{
64-
// if we have an escaped character, continue.
65-
i++;
66-
continue;
68+
// write the text
69+
string content = RewriteString(argMap, lex.GetValue(currentToken), args);
70+
s_printer.Write(content);
71+
break;
6772
}
68-
case '[':
73+
case TokenKind.CloseBracket:
6974
{
70-
// When we encounter a '[' it means we are probably going to change the color
71-
// so we need to write what we had up to this point.
72-
73-
// pop the location of the last paren from the stack
74-
// Write the message segment between the last paren and the current position
75-
int previousParenIndex = parens.Pop();
76-
WriteMessageSegment(message, args, argMap, previousParenIndex, i);
77-
78-
// Given a string that looks like [color! extracts the color and pushes it on the stack of colors
79-
ParseColor(message, colors, ref i);
80-
81-
// keep track of the latest parens that you saw
82-
unbalancedParens++;
83-
parens.Push(i);
84-
85-
continue;
75+
s_printer.ForegroundColor = colors.Pop();
76+
break;
8677
}
87-
case ']':
78+
case TokenKind.ColorDelimiter:
8879
{
89-
// at this point, we know where the color ended.
90-
// Write the message segment between the last paren and the current position
91-
int matchingbracket = parens.Pop();
92-
WriteMessageSegment(message, args, argMap, matchingbracket, i);
80+
s_printer.Write("!");
81+
break;
82+
}
83+
case TokenKind.OpenBracket:
84+
{
85+
currentTokenPosition++; // move to the next token, which should be a string token.
86+
currentToken = tokens[currentTokenPosition];
9387

94-
if (colors.Count == 0)
88+
// This case is []
89+
if (currentToken.Kind == TokenKind.CloseBracket)
9590
{
96-
throw new FormatException($"Missing expected ']' ");
91+
string content = RewriteString(argMap, "\\[\\]", args);
92+
s_printer.Write(content);
93+
currentTokenPosition++;
94+
break;
9795
}
98-
s_printer.ForegroundColor = colors.Pop();
9996

100-
parens.Push(i);
101-
unbalancedParens--;
102-
continue;
97+
// The token is not a close paren -- we should check and see what is the next parameter
98+
Token futureToken = tokens[currentTokenPosition + 1];
99+
if (futureToken.Kind == TokenKind.ColorDelimiter)
100+
{
101+
string colorName = lex.GetValue(currentToken);
102+
if (!s_consoleColorMap.TryGetValue(colorName, out ConsoleColor color))
103+
{
104+
throw new ArgumentException($"Unknown color: {colorName}");
105+
}
106+
107+
colors.Push(s_printer.ForegroundColor);
108+
s_printer.ForegroundColor = color;
109+
currentTokenPosition++; // skip over the ! token
110+
continue;
111+
}
112+
else if (futureToken.Kind == TokenKind.CloseBracket)
113+
{
114+
// check to see if we have a matching closing bracket (this can be covered by the check up-top)
115+
// the user wanted to write the actual text '[noColor]'
116+
117+
// construct an escaped string
118+
string content = RewriteString(argMap, "\\[" + lex.GetValue(currentToken) + "\\]", args);
119+
s_printer.Write(content);
120+
currentTokenPosition++; // skip over the close bracket token
121+
}
122+
123+
break;
103124
}
104125
}
105126
}
106-
107-
// at this point, the closing bracket might not have been found!
108-
if (unbalancedParens != 0)
109-
{
110-
throw new FormatException($"Missing expected ']' ");
111-
}
112-
113-
// write the last part, if any
114-
int finalParen = parens.Pop();
115-
WriteMessageSegment(message, args, argMap, finalParen, message.Length);
116127
}
117128

118-
private static void WriteMessageSegment(string message, object[] args, Dictionary<string, int> argMap, int startIndex, int currentIndex)
129+
private static void CheckFormat(Lexer lex)
119130
{
120-
// do we have anything to print?
121-
if (currentIndex - startIndex - 1 > 0)
122-
{
123-
string messageSegment = message.Substring(startIndex + 1, currentIndex - startIndex - 1);
124-
string content = RewriteString(argMap, messageSegment, args);
125-
s_printer.Write(content);
126-
}
127-
}
131+
Token[] tokens = lex.Tokenize();
128132

129-
private static void ParseColor(string message, Stack<ConsoleColor> colors, ref int currPos)
130-
{
131-
int textLength = message.Length;
132-
// find the color
133-
for (int pos = currPos + 1; pos < textLength; pos++)
133+
int brackets = 0;
134+
// check to see if the parens are balanced.
135+
for (int i = 0; i < tokens.Length; i++)
134136
{
135-
if (message[pos] == '!')
136-
{
137-
string colorString = message.Substring(currPos + 1, pos - currPos - 1);
138-
ConsoleColor color;
137+
if (tokens[i].Kind == TokenKind.OpenBracket) brackets++;
138+
if (tokens[i].Kind == TokenKind.CloseBracket) brackets--;
139139

140-
if (!s_consoleColorMap.TryGetValue(colorString, out color))
140+
// To nest you need to specify a color
141+
if (i > 0 && tokens[i].Kind == TokenKind.OpenBracket)
142+
{
143+
if (tokens[i - 1].Kind == tokens[i].Kind)
141144
{
142-
throw new ArgumentException($"Unknown color {colorString}");
145+
throw new FormatException($"Invalid format at position {tokens[i].Start}");
143146
}
144-
145-
colors.Push(s_printer.ForegroundColor);
146-
s_printer.ForegroundColor = color;
147-
148-
// set the position of the last character
149-
currPos = pos;
150-
break;
151147
}
152148
}
149+
150+
if (brackets != 0)
151+
{
152+
throw new FormatException("Invalid format, unbalanced paranthesis in the string");
153+
}
153154
}
154155

155156
private static string RewriteString(Dictionary<string, int> argMap, string content, params object[] args)
@@ -234,36 +235,46 @@ private static string RewriteString(Dictionary<string, int> argMap, string conte
234235
return string.Format(sb.ToString(), argument);
235236
}
236237

237-
private static Dictionary<string, int> CreateArgumentMap(string content)
238+
private static Dictionary<string, int> CreateArgumentMap(Token[] tokens, string content)
238239
{
239240
Dictionary<string, int> map = new Dictionary<string, int>();
240241

241242
int argCount = 0;
242-
int textLength = content.Length;
243-
for (int i = 0; i < textLength; i++)
243+
244+
foreach (Token tok in tokens)
244245
{
245-
if (content[i] == '{')
246+
// Skip over tokens that can't contain replacements.
247+
if (tok.Kind != TokenKind.String)
246248
{
249+
continue;
250+
}
251+
252+
int tokenTextLength = tok.End + 1;
253+
254+
for (int i = tok.Start; i < tokenTextLength; i++)
255+
{
256+
if (content[i] != '{')
257+
{
258+
continue;
259+
}
260+
247261
// '{' are escaped as '{{'
248-
if (i + 1 < textLength && content[i + 1] == '{')
262+
if (i + 1 < tokenTextLength && content[i + 1] == '{')
249263
{
250264
i++;
251265
continue;
252266
}
253267

254268
// find the matching closing curly bracket
255269
int pos = i;
256-
while (pos < textLength && content[pos++] != '}')
257-
{
258-
}
270+
while (pos < tokenTextLength && content[pos++] != '}') ;
259271

260272
if (content[pos - 1] != '}') // did not find matching '}'
261273
throw new ArgumentException(string.Format("Could not parse '{0}'", content));
262274

263275
string arg = content.Substring(i + 1, pos - i - 2);
264276

265-
int x;
266-
if (!int.TryParse(arg, out x))
277+
if (!int.TryParse(arg, out int x))
267278
{
268279
throw new ArgumentException(string.Format("Could not parse '{0}'", content));
269280
}

src/Lexer/Lexer.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Linq;
5+
using System.Runtime.InteropServices;
6+
using System.Text;
7+
8+
namespace OutputColorizer.Format
9+
{
10+
public class Lexer
11+
{
12+
private Token[] _tokens;
13+
14+
private readonly string Text;
15+
16+
public Lexer(string text)
17+
{
18+
Text = text;
19+
}
20+
21+
public Token[] Tokenize()
22+
{
23+
if (_tokens != null)
24+
{
25+
return _tokens;
26+
}
27+
28+
List<Token> tokens = new List<Token>();
29+
Token previousToken = default;
30+
31+
int currentIndex = 0, previousTokenEnd = 0;
32+
33+
while (currentIndex < Text.Length)
34+
{
35+
if (Text[currentIndex] == '\\')
36+
{
37+
// skip over the next character
38+
currentIndex++;
39+
}
40+
else
41+
{
42+
char token = Text[currentIndex];
43+
44+
if (token == ']' || token == '[' || token == '!')
45+
{
46+
// put whatever was before this token into a string token
47+
if (previousTokenEnd != currentIndex)
48+
{
49+
tokens.Add(new Token(TokenKind.String, previousTokenEnd, currentIndex - 1));
50+
}
51+
52+
// this will throw for invalid tokens.
53+
previousToken = new Token(token, currentIndex, currentIndex);
54+
tokens.Add(previousToken);
55+
previousTokenEnd = currentIndex + 1;
56+
}
57+
}
58+
59+
// continue if we need to.
60+
currentIndex++;
61+
}
62+
63+
// add a last segment if current index is different than previous token (i.e. some text after the last token)
64+
if (currentIndex != previousTokenEnd)
65+
{
66+
tokens.Add(new Token(TokenKind.String, previousTokenEnd, Text.Length - 1));
67+
}
68+
69+
_tokens = tokens.ToArray();
70+
return _tokens;
71+
}
72+
73+
public string GetValue(Token token)
74+
{
75+
return Text.Substring(token.Start, token.End - token.Start + 1);
76+
}
77+
78+
#if DEBUG
79+
public string WriteTokens()
80+
{
81+
StringBuilder sb = new StringBuilder();
82+
foreach (var item in _tokens)
83+
{
84+
sb.AppendLine($"{item.Kind} ({item.Start}-{item.End}): {GetValue(item)}");
85+
}
86+
return sb.ToString();
87+
}
88+
#endif
89+
}
90+
}

0 commit comments

Comments
 (0)