• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MorganKryze / ConsoleAppVisuals / 8158191062

05 Mar 2024 02:47PM UTC coverage: 86.165% (+0.9%) from 85.231%
8158191062

push

github

MorganKryze
📖 (readme) update roadmap

931 of 1144 branches covered (81.38%)

Branch coverage included in aggregate %.

1803 of 2029 relevant lines covered (88.86%)

412.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.36
/src/ConsoleAppVisuals/models/TextStyler.cs
1
/*
2
    GNU GPL License 2024 MorganKryze(Yann Vidamment)
3
    For full license information, please visit: https://github.com/MorganKryze/ConsoleAppVisuals/blob/main/LICENSE
4
*/
5
namespace ConsoleAppVisuals.Models;
6

7
/// <summary>
8
/// The class that styles any text with specified font files.
9
/// </summary>
10
/// <remarks>
11
/// For more information, refer to the following resources:
12
/// <list type="bullet">
13
/// <item><description><a href="https://morgankryze.github.io/ConsoleAppVisuals/">Documentation</a></description></item>
14
/// <item><description><a href="https://github.com/MorganKryze/ConsoleAppVisuals/blob/main/example/">Example Project</a></description></item>
15
/// </list>
16
/// </remarks>
17
public class TextStyler
18
{
19
    #region Constants: Paths, supported characters
20
    private const string DEFAULT_FONT_PATH = "ConsoleAppVisuals.fonts.";
21
    private const string DEFAULT_CONFIG_PATH = ".config.yml";
22
    private const string DEFAULT_ALPHABET_PATH = ".data.alphabet.txt";
23
    private const string DEFAULT_NUMBERS_PATH = ".data.numbers.txt";
24
    private const string DEFAULT_SYMBOLS_PATH = ".data.symbols.txt";
25
    private const string CONFIG_PATH = "config.yml";
26
    private const string ALPHABET_PATH = "data/alphabet.txt";
27
    private const string NUMBERS_PATH = "data/numbers.txt";
28
    private const string SYMBOLS_PATH = "data/symbols.txt";
29
    #endregion
30

31
    #region Fields: Font path, config, dictionary
32
    private readonly Font _font;
33
    private readonly string? _fontPath;
34
    private readonly FontYamlFile _config;
35
    private readonly Dictionary<char, string> _dictionary;
36
    private readonly string _supportedAlphabet;
37
    private readonly string _supportedNumbers;
38
    private readonly string _supportedSymbols;
39
    private readonly string _author;
40
    #endregion
41

42
    #region Properties: Dictionary
43
    /// <summary>
44
    /// The dictionary that stores the characters and their styled equivalent.
45
    /// </summary>
46
    public Dictionary<char, string> Dictionary => _dictionary;
12✔
47

48
    /// <summary>
49
    /// The font to use. Font.Custom if you want to use your own font.
50
    /// </summary>
51
    public Font Font => _font;
2✔
52

53
    /// <summary>
54
    /// The path to the font files. Null if the font is not custom.
55
    /// </summary>
56
    public string? FontPath => _fontPath;
2✔
57

58
    /// <summary>
59
    /// The supported alphabet by the font.
60
    /// </summary>
61
    public string SupportedAlphabet => _supportedAlphabet;
8,400✔
62

63
    /// <summary>
64
    /// The supported numbers by the font.
65
    /// </summary>
66
    public string SupportedNumbers => _supportedNumbers;
1,680✔
67

68
    /// <summary>
69
    /// The supported symbols by the font.
70
    /// </summary>
71
    public string SupportedSymbols => _supportedSymbols;
3,120✔
72

73
    /// <summary>
74
    /// All the supported characters by the font.
75
    /// </summary>
76
    public string SupportedChars => _supportedAlphabet + _supportedNumbers + _supportedSymbols;
2✔
77
    #endregion
78

79
    #region Constructor
80
    /// <summary>
81
    /// The constructor of the TextStyler class.
82
    /// </summary>
83
    /// <param name="source">The font to use. Font.Custom if you want to use your own font.</param>
84
    /// <param name="fontPath">ATTENTION: only use the path to the font files for custom fonts.</param>
85
    /// <param name="assembly">ATTENTION: Debug purposes only. Do not update it.</param>
86
    /// <exception cref="EmptyFileException">Thrown when the config.yml file is empty.</exception>
87
    /// <remarks>
88
    /// For more information, refer to the following resources:
89
    /// <list type="bullet">
90
    /// <item><description><a href="https://morgankryze.github.io/ConsoleAppVisuals/">Documentation</a></description></item>
91
    /// <item><description><a href="https://github.com/MorganKryze/ConsoleAppVisuals/blob/main/example/">Example Project</a></description></item>
92
    /// </list>
93
    /// </remarks>
94
    public TextStyler(
108✔
95
        Font source = Font.ANSI_Shadow,
108✔
96
        string? fontPath = null,
108✔
97
        Assembly? assembly = null
108✔
98
    )
108✔
99
    {
100
        if (source is Font.Custom && fontPath is null)
108✔
101
        {
102
            throw new ArgumentNullException(
2✔
103
                nameof(fontPath),
2✔
104
                "A Custom font path implies a non-null value for fontPath."
2✔
105
            );
2✔
106
        } 
107
        else if (source is not Font.Custom && fontPath is not null)
106✔
108
        {
109
            throw new ArgumentException(
2✔
110
                nameof(fontPath),
2✔
111
                "A non-Custom font implies a null value for fontPath."
2✔
112
            );
2✔
113
        }
114
        _font = source;
104✔
115
        _fontPath = fontPath;
104✔
116
        _dictionary = new Dictionary<char, string>();
104✔
117

118
        string yamlContent;
119
        if (source is Font.Custom)
104✔
120
        {
121
            yamlContent = File.ReadAllText(_fontPath + CONFIG_PATH);
30✔
122
        }
123
        else if (Enum.IsDefined(typeof(Font), source))
74✔
124
        {
125
            assembly ??= Assembly.GetExecutingAssembly();
72✔
126
            using var stream = assembly.GetManifestResourceStream(
72✔
127
                DEFAULT_FONT_PATH + source.ToString() + DEFAULT_CONFIG_PATH
72✔
128
            );
72✔
129
            using var reader = new StreamReader(stream ?? throw new FileNotFoundException());
72!
130
            yamlContent = reader.ReadToEnd();
72✔
131
        }
132
        else
133
        {
134
            throw new ArgumentException(
2✔
135
                nameof(source),
2✔
136
                "Font not recognized. Use Font.Custom for custom fonts."
2✔
137
            );
2✔
138
        }
139

140
        (_config, _supportedAlphabet, _supportedNumbers, _supportedSymbols, _author) = ParseYaml(
98✔
141
            yamlContent
98✔
142
        );
98✔
143

144
        BuildDictionary();
82✔
145
    }
82✔
146

147
    private (FontYamlFile, string, string, string, string) ParseYaml(string yamlContent)
148
    {
149
        FontYamlFile config;
150
        string alphabet;
151
        string numbers;
152
        string symbols;
153

154
        var deserializer = new DeserializerBuilder()
98✔
155
            .WithNamingConvention(CamelCaseNamingConvention.Instance)
98✔
156
            .Build();
98✔
157

158
        config = deserializer.Deserialize<FontYamlFile>(yamlContent);
98✔
159

160
        if (config.Name is null)
98✔
161
        {
162
            throw new FormatException("Name is not defined in the config.yml file.");
2✔
163
        }
164

165
        if (config.Author is null)
96✔
166
        {
167
            throw new FormatException(
2✔
168
                "Author is not defined in the config.yml file. If Unknown, use 'Unknown'."
2✔
169
            );
2✔
170
        }
171

172
        if (config.Height is null)
94✔
173
        {
174
            throw new FormatException("Height is not defined in the config.yml file.");
2✔
175
        }
176
        if (config.Height < 1)
92✔
177
        {
178
            throw new InvalidCastException("Height must be greater than 0.");
2✔
179
        }
180

181
        if (config.Chars is null)
90✔
182
        {
183
            throw new FormatException("Chars is not defined in the config.yml file.");
2✔
184
        }
185

186
        if (config.Chars["alphabet"] is null or "")
88✔
187
        {
188
            alphabet = "";
2✔
189
        }
190
        else
191
        {
192
            ValidateTextFile(
86✔
193
                _font is Font.Custom
86✔
194
                    ? _fontPath + ALPHABET_PATH
86✔
195
                    : DEFAULT_FONT_PATH + _font.ToString() + DEFAULT_ALPHABET_PATH,
86✔
196
                (int)config.Height
86✔
197
            );
86✔
198
            alphabet = config.Chars["alphabet"];
80✔
199
        }
200

201
        if (config.Chars["numbers"] is null or "")
82✔
202
        {
203
            numbers = "";
2✔
204
        }
205
        else
206
        {
207
            ValidateTextFile(
80✔
208
                _font is Font.Custom
80✔
209
                    ? _fontPath + NUMBERS_PATH
80✔
210
                    : DEFAULT_FONT_PATH + _font.ToString() + DEFAULT_NUMBERS_PATH,
80✔
211
                (int)config.Height
80✔
212
            );
80✔
213
            numbers = config.Chars["numbers"];
80✔
214
        }
215

216
        if (config.Chars["symbols"] is null or "")
82✔
217
        {
218
            symbols = "";
2✔
219
        }
220
        else
221
        {
222
            ValidateTextFile(
80✔
223
                _font is Font.Custom
80✔
224
                    ? _fontPath + SYMBOLS_PATH
80✔
225
                    : DEFAULT_FONT_PATH + _font.ToString() + DEFAULT_SYMBOLS_PATH,
80✔
226
                (int)config.Height
80✔
227
            );
80✔
228
            symbols = config.Chars["symbols"];
80✔
229
        }
230

231
        return (config, alphabet, numbers, symbols, config.Author);
82✔
232
    }
233

234
    private void ValidateTextFile(string filePath, int expectedHeight)
235
    {
236
        string[] lines;
237
        if (_font is Font.Custom)
246✔
238
        {
239
            lines = File.ReadAllLines(filePath);
30✔
240
        }
241
        else
242
        {
243
            var assembly = Assembly.GetExecutingAssembly();
216✔
244
            using var stream = assembly.GetManifestResourceStream(filePath);
216✔
245
            using var reader = new StreamReader(stream ?? throw new EmptyFileException());
216!
246
            lines = reader.ReadToEnd().Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
216✔
247
        }
248

249
        if (lines.Length % expectedHeight != 0)
246✔
250
        {
251
            throw new FormatException(
2✔
252
                $"Invalid number of lines in file: {filePath}. "
2✔
253
                    + $"Number of lines: {lines.Length}, "
2✔
254
                    + $"Expected multiple of: {expectedHeight}"
2✔
255
            );
2✔
256
        }
257

258
        for (int i = 0; i < lines.Length; i++)
78,376✔
259
        {
260
            var index = i + 1;
38,948✔
261
            if (index % expectedHeight == 0 && index != 1)
38,948✔
262
            {
263
                var line = lines[i].TrimEnd('\r', '\n');
6,490✔
264
                if (!line.EndsWith("@@"))
6,490✔
265
                {
266
                    var endOfLine = line.Length > 10 ? line.Substring(line.Length - 10) : line;
2!
267
                    throw new FormatException(
2✔
268
                        $"Character end line not ending with @@. Error in file: {filePath}, Line: {index}, End of line: {endOfLine}"
2✔
269
                    );
2✔
270
                }
271
            }
272
            else
273
            {
274
                var line = lines[i].TrimEnd('\r', '\n');
32,458✔
275
                if (!line.EndsWith("@"))
32,458✔
276
                {
277
                    var endOfLine = line.Length > 10 ? line.Substring(line.Length - 10) : line;
2!
278
                    throw new FormatException(
2✔
279
                        $"Character line not ending with @. Error in file: {filePath}, Line: {index}, End of line: {endOfLine}"
2✔
280
                    );
2✔
281
                }
282
            }
283
        }
284
    }
240✔
285

286
    private void BuildDictionary()
287
    {
288
        if (_supportedAlphabet != "")
82✔
289
        {
290
            AddAlphabetToDictionary();
80✔
291
        }
292

293
        if (_supportedNumbers != "")
82✔
294
        {
295
            AddNumbersToDictionary();
80✔
296
        }
297

298
        if (_supportedSymbols != "")
82✔
299
        {
300
            AddSymbolsToDictionary();
80✔
301
        }
302
    }
82✔
303

304
    private void AddAlphabetToDictionary()
305
    {
306
        List<string> alphabetStyled;
307
        alphabetStyled = ReadResourceLines(
80✔
308
            _font is Font.Custom ? ALPHABET_PATH : DEFAULT_ALPHABET_PATH
80✔
309
        );
80✔
310

311
        var alphabetStyledGrouped = alphabetStyled
80✔
312
            .Select((line, index) => new { line, index })
24,960✔
313
            .GroupBy(x => x.index / _config.Height)
24,960✔
314
            .Select(g => string.Join(Environment.NewLine, g.Select(x => x.line)))
29,120✔
315
            .ToList();
80✔
316

317
        for (int i = 0; i < SupportedAlphabet.Length; i++)
8,480✔
318
        {
319
            _dictionary.Add(SupportedAlphabet[i], alphabetStyledGrouped[i]);
4,160✔
320
        }
321
    }
80✔
322

323
    private void AddNumbersToDictionary()
324
    {
325
        List<string> numbersStyled;
326

327
        numbersStyled = ReadResourceLines(
80✔
328
            _font is Font.Custom ? NUMBERS_PATH : DEFAULT_NUMBERS_PATH
80✔
329
        );
80✔
330

331
        var numbersStyledGrouped = numbersStyled
80✔
332
            .Select((line, index) => new { line, index })
4,800✔
333
            .GroupBy(x => x.index / _config.Height)
4,800✔
334
            .Select(g => string.Join(Environment.NewLine, g.Select(x => x.line)))
5,600✔
335
            .ToList();
80✔
336

337
        for (int i = 0; i < SupportedNumbers.Length; i++)
1,760✔
338
        {
339
            _dictionary.Add(SupportedNumbers[i], numbersStyledGrouped[i]);
800✔
340
        }
341
    }
80✔
342

343
    private void AddSymbolsToDictionary()
344
    {
345
        List<string> symbolsStyled;
346

347
        symbolsStyled = ReadResourceLines(
80✔
348
            _font is Font.Custom ? SYMBOLS_PATH : DEFAULT_SYMBOLS_PATH
80✔
349
        );
80✔
350

351
        var symbolsStyledGrouped = symbolsStyled
80✔
352
            .Select((line, index) => new { line, index })
9,120✔
353
            .GroupBy(x => x.index / _config.Height)
9,120✔
354
            .Select(g => string.Join(Environment.NewLine, g.Select(x => x.line)))
10,640✔
355
            .ToList();
80✔
356

357
        for (int i = 0; i < SupportedSymbols.Length; i++)
3,200✔
358
        {
359
            _dictionary.Add(SupportedSymbols[i], symbolsStyledGrouped[i]);
1,520✔
360
        }
361
    }
80✔
362

363
    private List<string> ReadResourceLines(string path)
364
    {
365
        List<string> lines;
366

367
        if (_font is Font.Custom)
240✔
368
        {
369
            lines = File.ReadLines(_fontPath + path).ToList();
24✔
370
        }
371
        else
372
        {
373
            var assembly = Assembly.GetExecutingAssembly();
216✔
374
            using var stream = assembly.GetManifestResourceStream(
216✔
375
                DEFAULT_FONT_PATH + _font.ToString() + path
216✔
376
            );
216✔
377
            using var reader = new StreamReader(
216!
378
                stream
216✔
379
                    ?? throw new EmptyFileException(
216✔
380
                        "Font file not found or empty. No data extracted."
216✔
381
                    )
216✔
382
            );
216✔
383
            lines = reader
216✔
384
                .ReadToEnd()
216✔
385
                .Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)
216✔
386
                .ToList();
216✔
387
        }
388

389
        return CleanupLines(lines);
240✔
390
    }
391

392
    private static List<string> CleanupLines(List<string> lines)
393
    {
394
        for (int i = 0; i < lines.Count; i++)
78,240✔
395
        {
396
            lines[i] = lines[i].Replace("@", "");
38,880✔
397
        }
398

399
        return lines;
240✔
400
    }
401
    #endregion
402

403
    #region Methods: Style text
404
    /// <summary>
405
    /// Styles the given text with the font files.
406
    /// </summary>
407
    /// <param name="text">The text to style.</param>
408
    /// <returns>The styled text as a string array.</returns>
409
    /// <remarks>
410
    /// For more information, refer to the following resources:
411
    /// <list type="bullet">
412
    /// <item><description><a href="https://morgankryze.github.io/ConsoleAppVisuals/">Documentation</a></description></item>
413
    /// <item><description><a href="https://github.com/MorganKryze/ConsoleAppVisuals/blob/main/example/">Example Project</a></description></item>
414
    /// </list>
415
    /// </remarks>
416
    public string[] Style(string text)
417
    {
418
        var lines = new List<string[]>();
16✔
419
        foreach (char c in text)
426✔
420
        {
421
            if (_dictionary.ContainsKey(c))
198✔
422
                lines.Add(
196✔
423
                    _dictionary[c].Split(new[] { Environment.NewLine }, StringSplitOptions.None)
196✔
424
                );
196✔
425
            else
426
                throw new NotSupportedCharException($"The character '{c}' is not supported.");
2✔
427
        }
428

429
        var result = new List<string>();
14✔
430
        for (int i = 0; i < lines[0].Length; i++)
196✔
431
        {
432
            var sb = new StringBuilder();
84✔
433
            foreach (var line in lines)
2,208✔
434
            {
435
                sb.Append(line[i]);
1,020✔
436
            }
437
            result.Add(sb.ToString());
84✔
438
        }
439

440
        return result.ToArray();
14✔
441
    }
442

443
    /// <summary>
444
    /// Get the info of the actual style (from the config.yml file).
445
    /// </summary>
446
    /// <returns>A string compiling these pieces of information.</returns>
447
    /// <exception cref="EmptyFileException">Thrown when the config.yml file is empty.</exception>
448
    /// <remarks>
449
    /// For more information, refer to the following resources:
450
    /// <list type="bullet">
451
    /// <item><description><a href="https://morgankryze.github.io/ConsoleAppVisuals/">Documentation</a></description></item>
452
    /// <item><description><a href="https://github.com/MorganKryze/ConsoleAppVisuals/blob/main/example/">Example Project</a></description></item>
453
    /// </list>
454
    /// </remarks>
455
    public override string ToString()
456
    {
457
        var sb = new StringBuilder();
2✔
458
        sb.AppendLine($"Name: {_config.Name}");
2✔
459
        sb.AppendLine($"Author: {_config.Author}");
2✔
460
        sb.AppendLine($"Height: {_config.Height}");
2✔
461
        if (_config.Chars != null)
2✔
462
        {
463
            sb.AppendLine($"List of supported chars:\n");
2✔
464

465
            foreach (KeyValuePair<string, string> pair in _config.Chars)
16✔
466
            {
467
                sb.AppendLine($"File: {pair.Key}, supported chars: {pair.Value}");
6✔
468
            }
469
        }
470
        return sb.ToString();
2✔
471
    }
472
    #endregion
473
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc