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

MorganKryze / ConsoleAppVisuals / 8142798391

04 Mar 2024 03:31PM UTC coverage: 84.112% (-1.1%) from 85.231%
8142798391

push

github

MorganKryze
🌟 can specify default of custom font | remove textstyler from core | add author, supported chars to config.yml files | add security on the import of fonts

896 of 1126 branches covered (79.57%)

Branch coverage included in aggregate %.

113 of 154 new or added lines in 4 files covered. (73.38%)

1 existing line in 1 file now uncovered.

1767 of 2040 relevant lines covered (86.62%)

380.16 hits per line

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

79.79
/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>
NEW
51
    public Font Source => _source;
×
52

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

58
    /// <summary>
59
    /// The supported alphabet by the font.
60
    /// </summary>
61
    public string SupportedAlphabet => _supportedAlphabet;
3,975✔
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>
NEW
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
        {
NEW
102
            throw new ArgumentNullException(
×
NEW
103
                nameof(fontPath),
×
NEW
104
                "No font path provided for a custom font."
×
NEW
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
        {
NEW
127
            throw new ArgumentException(
×
NEW
128
                nameof(source),
×
NEW
129
                "Font not recognized. Use Font.Custom for custom fonts."
×
NEW
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✔
NEW
155
        catch (YamlException ex)
×
156
        {
NEW
157
            throw new YamlException(
×
NEW
158
                "The config.yml file is not in the correct format. Check that the file is a YAML file.",
×
NEW
159
                ex
×
NEW
160
            );
×
161
        }
NEW
162
        catch (InvalidCastException ex)
×
163
        {
NEW
164
            throw new InvalidCastException(
×
NEW
165
                "The config.yml file is not in the correct format. Consider reading the documentation.",
×
NEW
166
                ex
×
NEW
167
            );
×
168
        }
169
        if (config.Height is null)
78✔
170
        {
171
            throw new FormatException("Height is not defined in the config.yml file.");
3✔
172
        }
173
        if (config.Height < 1)
75!
174
        {
NEW
175
            throw new FormatException("Height must be greater than 0.");
×
176
        }
177
        if (config.Chars is null)
75!
178
        {
NEW
179
            throw new FormatException("Chars is not defined in the config.yml file.");
×
180
        }
181
        ValidateTextFile(
75✔
182
            _source is Font.Custom
75✔
183
                ? _fontPath + ALPHABET_PATH
75✔
184
                : DEFAULT_FONT_PATH + _source.ToString() + DEFAULT_ALPHABET_PATH,
75✔
185
            (int)config.Height
75✔
186
        );
75✔
187
        alphabet = config.Chars["alphabet"];
75✔
188

189
        ValidateTextFile(
75✔
190
            _source is Font.Custom
75✔
191
                ? _fontPath + NUMBERS_PATH
75✔
192
                : DEFAULT_FONT_PATH + _source.ToString() + DEFAULT_NUMBERS_PATH,
75✔
193
            (int)config.Height
75✔
194
        );
75✔
195
        numbers = config.Chars["numbers"];
75✔
196

197
        ValidateTextFile(
75✔
198
            _source is Font.Custom
75✔
199
                ? _fontPath + SYMBOLS_PATH
75✔
200
                : DEFAULT_FONT_PATH + _source.ToString() + DEFAULT_SYMBOLS_PATH,
75✔
201
            (int)config.Height
75✔
202
        );
75✔
203
        symbols = config.Chars["symbols"];
75✔
204

205
        if (config.Author is null)
75!
206
        {
NEW
207
            throw new FormatException("Author is not defined in the config.yml file.");
×
208
        }
209

210
        return (config, alphabet, numbers, symbols, (string)config.Author);
75✔
211
    }
212

213
    private void ValidateTextFile(string filePath, int expectedHeight)
214
    {
215
        string[] lines;
216
        if (_source is Font.Custom)
225✔
217
        {
218
            lines = File.ReadAllLines(filePath);
18✔
219
        }
220
        else
221
        {
222
            var assembly = Assembly.GetExecutingAssembly();
207✔
223
            using var stream = assembly.GetManifestResourceStream(filePath);
207✔
224
            using var reader = new StreamReader(stream ?? throw new EmptyFileException());
207!
225
            lines = reader.ReadToEnd().Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
207✔
226
        }
227

228
        for (int i = 0; i < lines.Length; i++)
49,950✔
229
        {
230
            var index = i + 1;
24,750✔
231
            if (index % expectedHeight == 0 && index == 1)
24,750!
232
            {
NEW
233
                var line = lines[i].TrimEnd('\r', '\n');
×
NEW
234
                if (!line.EndsWith("@@"))
×
235
                {
NEW
236
                    var endOfLine = line.Length > 10 ? line.Substring(line.Length - 10) : line;
×
NEW
237
                    throw new FormatException(
×
NEW
238
                        $"Character end line not ending with @@. Error in file: {filePath}, Line: {index}, End of line: {endOfLine}"
×
NEW
239
                    );
×
240
                }
241
            }
242
            else
243
            {
244
                var line = lines[i].TrimEnd('\r', '\n');
24,750✔
245
                if (!line.EndsWith("@"))
24,750!
246
                {
NEW
247
                    var endOfLine = line.Length > 10 ? line.Substring(line.Length - 10) : line;
×
NEW
248
                    throw new FormatException(
×
NEW
249
                        $"Character line not ending with @. Error in file: {filePath}, Line: {index}, End of line: {endOfLine}"
×
NEW
250
                    );
×
251
                }
252
            }
253
        }
254

255
        if (lines.Length % expectedHeight != 0)
225!
256
        {
NEW
257
            throw new FormatException(
×
NEW
258
                $"Invalid number of lines in file: {filePath}. "
×
NEW
259
                    + $"Number of lines: {lines.Length}, "
×
NEW
260
                    + $"Expected multiple of: {expectedHeight}"
×
NEW
261
            );
×
262
        }
263
    }
225✔
264

265
    private void BuildDictionary()
266
    {
267
        List<string> alphabetStyled;
268
        List<string> numbersStyled;
269
        List<string> symbolsStyled;
270

271
        if (_source is Font.Custom)
75✔
272
        {
273
            alphabetStyled = ReadResourceLines(ALPHABET_PATH);
6✔
274
            numbersStyled = ReadResourceLines(NUMBERS_PATH);
6✔
275
            symbolsStyled = ReadResourceLines(SYMBOLS_PATH);
6✔
276
        }
277
        else
278
        {
279
            alphabetStyled = ReadResourceLines(DEFAULT_ALPHABET_PATH);
69✔
280
            numbersStyled = ReadResourceLines(DEFAULT_NUMBERS_PATH);
69✔
281
            symbolsStyled = ReadResourceLines(DEFAULT_SYMBOLS_PATH);
69✔
282
        }
283
        if (_config.Chars is null)
75!
UNCOV
284
            throw new EmptyFileException("The config.yml file is empty.");
×
285

286
        var alphabetStyledGrouped = alphabetStyled
75✔
287
            .Select((line, index) => new { line, index })
11,700✔
288
            .GroupBy(x => x.index / _config.Height)
11,700✔
289
            .Select(g => string.Join(Environment.NewLine, g.Select(x => x.line)))
13,650✔
290
            .ToList();
75✔
291
        var numbersStyledGrouped = numbersStyled
75✔
292
            .Select((line, index) => new { line, index })
4,500✔
293
            .GroupBy(x => x.index / _config.Height)
4,500✔
294
            .Select(g => string.Join(Environment.NewLine, g.Select(x => x.line)))
5,250✔
295
            .ToList();
75✔
296
        var symbolsStyledGrouped = symbolsStyled
75✔
297
            .Select((line, index) => new { line, index })
8,550✔
298
            .GroupBy(x => x.index / _config.Height)
8,550✔
299
            .Select(g => string.Join(Environment.NewLine, g.Select(x => x.line)))
9,975✔
300
            .ToList();
75✔
301

302
        for (int i = 0; i < SupportedAlphabet.Length; i++)
4,050✔
303
            _dictionary.Add(SupportedAlphabet[i], alphabetStyledGrouped[i]);
1,950✔
304
        for (int i = 0; i < SupportedNumbers.Length; i++)
1,650✔
305
            _dictionary.Add(SupportedNumbers[i], numbersStyledGrouped[i]);
750✔
306
        for (int i = 0; i < SupportedSymbols.Length; i++)
3,000✔
307
            _dictionary.Add(SupportedSymbols[i], symbolsStyledGrouped[i]);
1,425✔
308
    }
75✔
309

310
    private List<string> ReadResourceLines(string path)
311
    {
312
        List<string> lines;
313

314
        if (_source is Font.Custom)
225✔
315
        {
316
            lines = File.ReadLines(_fontPath + path).ToList();
18✔
317
        }
318
        else
319
        {
320
            var assembly = Assembly.GetExecutingAssembly();
207✔
321
            using var stream = assembly.GetManifestResourceStream(
207✔
322
                DEFAULT_FONT_PATH + _source.ToString() + path
207✔
323
            );
207✔
324
            using var reader = new StreamReader(
207!
325
                stream
207✔
326
                    ?? throw new EmptyFileException(
207✔
327
                        "Font file not found or empty. No data extracted."
207✔
328
                    )
207✔
329
            );
207✔
330
            lines = reader
207✔
331
                .ReadToEnd()
207✔
332
                .Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)
207✔
333
                .ToList();
207✔
334
        }
335

336
        return CleanupLines(lines);
225✔
337
    }
338

339
    private static List<string> CleanupLines(List<string> lines)
340
    {
341
        for (int i = 0; i < lines.Count; i++)
49,950✔
342
        {
343
            lines[i] = lines[i].Replace("@", "");
24,750✔
344
        }
345

346
        return lines;
225✔
347
    }
348
    #endregion
349

350
    #region Methods: Style text
351
    /// <summary>
352
    /// Styles the given text with the font files.
353
    /// </summary>
354
    /// <param name="text">The text to style.</param>
355
    /// <returns>The styled text.</returns>
356
    /// <remarks>
357
    /// For more information, refer to the following resources:
358
    /// <list type="bullet">
359
    /// <item><description><a href="https://morgankryze.github.io/ConsoleAppVisuals/">Documentation</a></description></item>
360
    /// <item><description><a href="https://github.com/MorganKryze/ConsoleAppVisuals/blob/main/example/">Example Project</a></description></item>
361
    /// </list>
362
    /// </remarks>
363
    public string StyleTextToString(string text)
364
    {
365
        text = text.ToLower();
6✔
366
        var lines = new List<string[]>();
6✔
367
        foreach (char c in text)
159✔
368
        {
369
            if (_dictionary.ContainsKey(c))
75✔
370
                lines.Add(
72✔
371
                    _dictionary[c].Split(new[] { Environment.NewLine }, StringSplitOptions.None)
72✔
372
                );
72✔
373
            else
374
                throw new NotSupportedCharException($"The character '{c}' is not supported.");
3✔
375
        }
376

377
        var sb = new StringBuilder();
3✔
378
        for (int i = 0; i < lines[0].Length; i++)
42✔
379
        {
380
            foreach (var line in lines)
432✔
381
            {
382
                sb.Append(line[i]);
198✔
383
            }
384
            sb.AppendLine();
18✔
385
        }
386

387
        return sb.ToString();
3✔
388
    }
389

390
    /// <summary>
391
    /// Styles the given text with the font files.
392
    /// </summary>
393
    /// <param name="text">The text to style.</param>
394
    /// <returns>The styled text as a string array.</returns>
395
    /// <remarks>
396
    /// For more information, refer to the following resources:
397
    /// <list type="bullet">
398
    /// <item><description><a href="https://morgankryze.github.io/ConsoleAppVisuals/">Documentation</a></description></item>
399
    /// <item><description><a href="https://github.com/MorganKryze/ConsoleAppVisuals/blob/main/example/">Example Project</a></description></item>
400
    /// </list>
401
    /// </remarks>
402
    public string[] StyleTextToStringArray(string text)
403
    {
404
        text = text.ToLower();
24✔
405
        var lines = new List<string[]>();
24✔
406
        foreach (char c in text)
639✔
407
        {
408
            if (_dictionary.ContainsKey(c))
297✔
409
                lines.Add(
294✔
410
                    _dictionary[c].Split(new[] { Environment.NewLine }, StringSplitOptions.None)
294✔
411
                );
294✔
412
            else
413
                throw new NotSupportedCharException($"The character '{c}' is not supported.");
3✔
414
        }
415

416
        var result = new List<string>();
21✔
417
        for (int i = 0; i < lines[0].Length; i++)
294✔
418
        {
419
            var sb = new StringBuilder();
126✔
420
            foreach (var line in lines)
3,312✔
421
            {
422
                sb.Append(line[i]);
1,530✔
423
            }
424
            result.Add(sb.ToString());
126✔
425
        }
426

427
        return result.ToArray();
21✔
428
    }
429

430
    /// <summary>
431
    /// Get the info of the actual style (from the config.yml file).
432
    /// </summary>
433
    /// <returns>A string compiling these pieces of information.</returns>
434
    /// <exception cref="EmptyFileException">Thrown when the config.yml file is empty.</exception>
435
    /// <remarks>
436
    /// For more information, refer to the following resources:
437
    /// <list type="bullet">
438
    /// <item><description><a href="https://morgankryze.github.io/ConsoleAppVisuals/">Documentation</a></description></item>
439
    /// <item><description><a href="https://github.com/MorganKryze/ConsoleAppVisuals/blob/main/example/">Example Project</a></description></item>
440
    /// </list>
441
    /// </remarks>
442
    public override string ToString()
443
    {
444
        var sb = new StringBuilder();
3✔
445
        sb.AppendLine($"Name: {_config.Name}");
3✔
446
        sb.AppendLine($"Author: {_config.Author}");
3✔
447
        sb.AppendLine($"Height: {_config.Height}");
3✔
448
        if (_config.Chars != null)
3✔
449
        {
450
            sb.AppendLine($"List of supported chars:\n");
3✔
451

452
            foreach (KeyValuePair<string, string> pair in _config.Chars)
24✔
453
            {
454
                sb.AppendLine($"File: {pair.Key}, supported chars: {pair.Value}");
9✔
455
            }
456
        }
457
        return sb.ToString();
3✔
458
    }
459
    #endregion
460
}
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