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

MorganKryze / ConsoleAppVisuals / 8146691094

04 Mar 2024 08:37PM UTC coverage: 84.242% (-1.0%) from 85.231%
8146691094

push

github

MorganKryze
🌟 add Stop font

913 of 1146 branches covered (79.67%)

Branch coverage included in aggregate %.

1776 of 2046 relevant lines covered (86.8%)

453.32 hits per line

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

78.64
/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 _source;
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;
18✔
47

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

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

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

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

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

73
    /// <summary>
74
    /// The author of the font.
75
    /// </summary>
76
    public string Author => _author;
×
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(
84✔
95
        Font source = Font.ANSI_Shadow,
84✔
96
        string? fontPath = null,
84✔
97
        Assembly? assembly = null
84✔
98
    )
84✔
99
    {
100
        if (source is Font.Custom && fontPath is null)
84!
101
        {
102
            throw new ArgumentNullException(
×
103
                nameof(fontPath),
×
104
                "No font path provided for a custom font."
×
105
            );
×
106
        }
107
        _source = source;
84✔
108
        _fontPath = fontPath;
84✔
109
        _dictionary = new Dictionary<char, string>();
84✔
110

111
        string yamlContent;
112
        if (source is Font.Custom)
84✔
113
        {
114
            yamlContent = File.ReadAllText(_fontPath + CONFIG_PATH);
15✔
115
        }
116
        else if (Enum.IsDefined(typeof(Font), source))
69!
117
        {
118
            assembly ??= Assembly.GetExecutingAssembly();
69✔
119
            using var stream = assembly.GetManifestResourceStream(
69✔
120
                DEFAULT_FONT_PATH + source.ToString() + DEFAULT_CONFIG_PATH
69✔
121
            );
69✔
122
            using var reader = new StreamReader(stream ?? throw new FileNotFoundException());
69!
123
            yamlContent = reader.ReadToEnd();
69✔
124
        }
125
        else
126
        {
127
            throw new ArgumentException(
×
128
                nameof(source),
×
129
                "Font not recognized. Use Font.Custom for custom fonts."
×
130
            );
×
131
        }
132

133
        (_config, _supportedAlphabet, _supportedNumbers, _supportedSymbols, _author) = ParseYaml(
78✔
134
            yamlContent
78✔
135
        );
78✔
136

137
        BuildDictionary();
75✔
138
    }
75✔
139

140
    private (FontYamlFile, string, string, string, string) ParseYaml(string yamlContent)
141
    {
142
        FontYamlFile config;
143
        string alphabet;
144
        string numbers;
145
        string symbols;
146

147
        var deserializer = new DeserializerBuilder()
78✔
148
            .WithNamingConvention(CamelCaseNamingConvention.Instance)
78✔
149
            .Build();
78✔
150

151
        try
152
        {
153
            config = deserializer.Deserialize<FontYamlFile>(yamlContent);
78✔
154
        }
78✔
155
        catch (YamlException e)
×
156
        {
157
            throw new YamlException(
×
158
                "The config.yml file is not in the correct format. Check that the file is a YAML file.",
×
159
                e
×
160
            );
×
161
        }
162
        catch (InvalidCastException e)
×
163
        {
164
            throw new InvalidCastException(
×
165
                "The config.yml file is not in the correct format. Consider reading the documentation.",
×
166
                e
×
167
            );
×
168
        }
169

170
        if (config.Height is null)
78✔
171
        {
172
            throw new FormatException("Height is not defined in the config.yml file.");
3✔
173
        }
174
        if (config.Height < 1)
75!
175
        {
176
            throw new FormatException("Height must be greater than 0.");
×
177
        }
178
        if (config.Chars is null)
75!
179
        {
180
            throw new FormatException("Chars is not defined in the config.yml file.");
×
181
        }
182

183
        if (config.Chars["alphabet"] is null or "")
75!
184
        {
185
            alphabet = "";
×
186
        }
187
        else
188
        {
189
            ValidateTextFile(
75✔
190
                _source is Font.Custom
75✔
191
                    ? _fontPath + ALPHABET_PATH
75✔
192
                    : DEFAULT_FONT_PATH + _source.ToString() + DEFAULT_ALPHABET_PATH,
75✔
193
                (int)config.Height
75✔
194
            );
75✔
195
            alphabet = config.Chars["alphabet"];
75✔
196
        }
197

198
        if (config.Chars["numbers"] is null or "")
75!
199
        {
200
            numbers = "";
×
201
        }
202
        else
203
        {
204
            ValidateTextFile(
75✔
205
                _source is Font.Custom
75✔
206
                    ? _fontPath + NUMBERS_PATH
75✔
207
                    : DEFAULT_FONT_PATH + _source.ToString() + DEFAULT_NUMBERS_PATH,
75✔
208
                (int)config.Height
75✔
209
            );
75✔
210
            numbers = config.Chars["numbers"];
75✔
211
        }
212

213
        if (config.Chars["symbols"] is null or "")
75!
214
        {
215
            symbols = "";
×
216
        }
217
        else
218
        {
219
            ValidateTextFile(
75✔
220
                _source is Font.Custom
75✔
221
                    ? _fontPath + SYMBOLS_PATH
75✔
222
                    : DEFAULT_FONT_PATH + _source.ToString() + DEFAULT_SYMBOLS_PATH,
75✔
223
                (int)config.Height
75✔
224
            );
75✔
225
            symbols = config.Chars["symbols"];
75✔
226
        }
227

228
        if (config.Author is null)
75!
229
        {
230
            throw new FormatException(
×
231
                "Author is not defined in the config.yml file. If Unknown, use 'Unknown'."
×
232
            );
×
233
        }
234

235
        return (config, alphabet, numbers, symbols, config.Author);
75✔
236
    }
237

238
    private void ValidateTextFile(string filePath, int expectedHeight)
239
    {
240
        string[] lines;
241
        if (_source is Font.Custom)
225✔
242
        {
243
            lines = File.ReadAllLines(filePath);
18✔
244
        }
245
        else
246
        {
247
            var assembly = Assembly.GetExecutingAssembly();
207✔
248
            using var stream = assembly.GetManifestResourceStream(filePath);
207✔
249
            using var reader = new StreamReader(stream ?? throw new EmptyFileException());
207!
250
            lines = reader.ReadToEnd().Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
207✔
251
        }
252

253
        for (int i = 0; i < lines.Length; i++)
71,478✔
254
        {
255
            var index = i + 1;
35,514✔
256
            if (index % expectedHeight == 0 && index == 1)
35,514!
257
            {
258
                var line = lines[i].TrimEnd('\r', '\n');
×
259
                if (!line.EndsWith("@@"))
×
260
                {
261
                    var endOfLine = line.Length > 10 ? line.Substring(line.Length - 10) : line;
×
262
                    throw new FormatException(
×
263
                        $"Character end line not ending with @@. Error in file: {filePath}, Line: {index}, End of line: {endOfLine}"
×
264
                    );
×
265
                }
266
            }
267
            else
268
            {
269
                var line = lines[i].TrimEnd('\r', '\n');
35,514✔
270
                if (!line.EndsWith("@"))
35,514!
271
                {
272
                    var endOfLine = line.Length > 10 ? line.Substring(line.Length - 10) : line;
×
273
                    throw new FormatException(
×
274
                        $"Character line not ending with @. Error in file: {filePath}, Line: {index}, End of line: {endOfLine}"
×
275
                    );
×
276
                }
277
            }
278
        }
279

280
        if (lines.Length % expectedHeight != 0)
225!
281
        {
282
            throw new FormatException(
×
283
                $"Invalid number of lines in file: {filePath}. "
×
284
                    + $"Number of lines: {lines.Length}, "
×
285
                    + $"Expected multiple of: {expectedHeight}"
×
286
            );
×
287
        }
288
    }
225✔
289

290
    private void BuildDictionary()
291
    {
292
        if (_supportedAlphabet != "")
75✔
293
        {
294
            AddAlphabetToDictionary();
75✔
295
        }
296

297
        if (_supportedNumbers != "")
75✔
298
        {
299
            AddNumbersToDictionary();
75✔
300
        }
301

302
        if (_supportedSymbols != "")
75✔
303
        {
304
            AddSymbolsToDictionary();
75✔
305
        }
306
    }
75✔
307

308
    private void AddAlphabetToDictionary()
309
    {
310
        List<string> alphabetStyled;
311
        alphabetStyled = ReadResourceLines(
75✔
312
            _source is Font.Custom ? ALPHABET_PATH : DEFAULT_ALPHABET_PATH
75✔
313
        );
75✔
314

315
        var alphabetStyledGrouped = alphabetStyled
75✔
316
            .Select((line, index) => new { line, index })
22,464✔
317
            .GroupBy(x => x.index / _config.Height)
22,464✔
318
            .Select(g => string.Join(Environment.NewLine, g.Select(x => x.line)))
26,208✔
319
            .ToList();
75✔
320

321
        for (int i = 0; i < SupportedAlphabet.Length; i++)
7,638✔
322
        {
323
            _dictionary.Add(SupportedAlphabet[i], alphabetStyledGrouped[i]);
3,744✔
324
        }
325
    }
75✔
326

327
    private void AddNumbersToDictionary()
328
    {
329
        List<string> numbersStyled;
330

331
        numbersStyled = ReadResourceLines(
75✔
332
            _source is Font.Custom ? NUMBERS_PATH : DEFAULT_NUMBERS_PATH
75✔
333
        );
75✔
334

335
        var numbersStyledGrouped = numbersStyled
75✔
336
            .Select((line, index) => new { line, index })
4,500✔
337
            .GroupBy(x => x.index / _config.Height)
4,500✔
338
            .Select(g => string.Join(Environment.NewLine, g.Select(x => x.line)))
5,250✔
339
            .ToList();
75✔
340

341
        for (int i = 0; i < SupportedNumbers.Length; i++)
1,650✔
342
        {
343
            _dictionary.Add(SupportedNumbers[i], numbersStyledGrouped[i]);
750✔
344
        }
345
    }
75✔
346

347
    private void AddSymbolsToDictionary()
348
    {
349
        List<string> symbolsStyled;
350

351
        symbolsStyled = ReadResourceLines(
75✔
352
            _source is Font.Custom ? SYMBOLS_PATH : DEFAULT_SYMBOLS_PATH
75✔
353
        );
75✔
354

355
        var symbolsStyledGrouped = symbolsStyled
75✔
356
            .Select((line, index) => new { line, index })
8,550✔
357
            .GroupBy(x => x.index / _config.Height)
8,550✔
358
            .Select(g => string.Join(Environment.NewLine, g.Select(x => x.line)))
9,975✔
359
            .ToList();
75✔
360

361
        for (int i = 0; i < SupportedSymbols.Length; i++)
3,000✔
362
        {
363
            _dictionary.Add(SupportedSymbols[i], symbolsStyledGrouped[i]);
1,425✔
364
        }
365
    }
75✔
366

367
    private List<string> ReadResourceLines(string path)
368
    {
369
        List<string> lines;
370

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

393
        return CleanupLines(lines);
225✔
394
    }
395

396
    private static List<string> CleanupLines(List<string> lines)
397
    {
398
        for (int i = 0; i < lines.Count; i++)
71,478✔
399
        {
400
            lines[i] = lines[i].Replace("@", "");
35,514✔
401
        }
402

403
        return lines;
225✔
404
    }
405
    #endregion
406

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

433
        var sb = new StringBuilder();
3✔
434
        for (int i = 0; i < lines[0].Length; i++)
42✔
435
        {
436
            foreach (var line in lines)
432✔
437
            {
438
                sb.Append(line[i]);
198✔
439
            }
440
            sb.AppendLine();
18✔
441
        }
442

443
        return sb.ToString();
3✔
444
    }
445

446
    /// <summary>
447
    /// Styles the given text with the font files.
448
    /// </summary>
449
    /// <param name="text">The text to style.</param>
450
    /// <returns>The styled text as a string array.</returns>
451
    /// <remarks>
452
    /// For more information, refer to the following resources:
453
    /// <list type="bullet">
454
    /// <item><description><a href="https://morgankryze.github.io/ConsoleAppVisuals/">Documentation</a></description></item>
455
    /// <item><description><a href="https://github.com/MorganKryze/ConsoleAppVisuals/blob/main/example/">Example Project</a></description></item>
456
    /// </list>
457
    /// </remarks>
458
    public string[] StyleTextToStringArray(string text)
459
    {
460
        var lines = new List<string[]>();
24✔
461
        foreach (char c in text)
639✔
462
        {
463
            if (_dictionary.ContainsKey(c))
297✔
464
                lines.Add(
294✔
465
                    _dictionary[c].Split(new[] { Environment.NewLine }, StringSplitOptions.None)
294✔
466
                );
294✔
467
            else
468
                throw new NotSupportedCharException($"The character '{c}' is not supported.");
3✔
469
        }
470

471
        var result = new List<string>();
21✔
472
        for (int i = 0; i < lines[0].Length; i++)
294✔
473
        {
474
            var sb = new StringBuilder();
126✔
475
            foreach (var line in lines)
3,312✔
476
            {
477
                sb.Append(line[i]);
1,530✔
478
            }
479
            result.Add(sb.ToString());
126✔
480
        }
481

482
        return result.ToArray();
21✔
483
    }
484

485
    /// <summary>
486
    /// Get the info of the actual style (from the config.yml file).
487
    /// </summary>
488
    /// <returns>A string compiling these pieces of information.</returns>
489
    /// <exception cref="EmptyFileException">Thrown when the config.yml file is empty.</exception>
490
    /// <remarks>
491
    /// For more information, refer to the following resources:
492
    /// <list type="bullet">
493
    /// <item><description><a href="https://morgankryze.github.io/ConsoleAppVisuals/">Documentation</a></description></item>
494
    /// <item><description><a href="https://github.com/MorganKryze/ConsoleAppVisuals/blob/main/example/">Example Project</a></description></item>
495
    /// </list>
496
    /// </remarks>
497
    public override string ToString()
498
    {
499
        var sb = new StringBuilder();
3✔
500
        sb.AppendLine($"Name: {_config.Name}");
3✔
501
        sb.AppendLine($"Author: {_config.Author}");
3✔
502
        sb.AppendLine($"Height: {_config.Height}");
3✔
503
        if (_config.Chars != null)
3✔
504
        {
505
            sb.AppendLine($"List of supported chars:\n");
3✔
506

507
            foreach (KeyValuePair<string, string> pair in _config.Chars)
24✔
508
            {
509
                sb.AppendLine($"File: {pair.Key}, supported chars: {pair.Value}");
9✔
510
            }
511
        }
512
        return sb.ToString();
3✔
513
    }
514
    #endregion
515
}
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