diff --git a/README.md b/README.md index 9cfca38..aea7018 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ import 'package:terminal_color_parser/terminal_color_parser.dart'; final coloredText = ColorParser('Hello, \x1B[32mworld\x1B[0m!').parse(); print(coloredText); -// ==> ColoredText("Hello, ", 0:0, , ()), ColoredText("world", 32:0, , ()), ColoredText("!", 0:0, , ())] +// ==> ColorToken("Hello, ", 0:0, , ()), ColorToken("world", 32:0, , ()), ColorToken("!", 0:0, , ())] var i = 0; for (final token in coloredText) { diff --git a/example/example.dart b/example/example.dart index 41402e6..629f5c7 100644 --- a/example/example.dart +++ b/example/example.dart @@ -1,10 +1,11 @@ +import 'package:terminal_color_parser/src/color.dart'; import 'package:terminal_color_parser/terminal_color_parser.dart'; void main(List args) async { final coloredText = ColorParser('Hello, \x1B[32mworld\x1B[0m!').parse(); print(coloredText); - // ==> ColoredText("Hello, ", 0:0, , ()), ColoredText("world", 32:0, , ()), ColoredText("!", 0:0, , ())] + // ==> ColorToken("Hello, ", 0:0, , ()), ColorToken("world", 32:0, , ()), ColorToken("!", 0:0, , ())] var i = 0; for (final token in coloredText) { @@ -17,14 +18,13 @@ void main(List args) async { // Construct your own colored text final tokens = [ - ColorToken(text: 'Hello, ', fgColor: 0, bgColor: 0), + ColorToken(text: 'Hello, '), ColorToken( text: 'world', - fgColor: 32, - bgColor: 0, + fgColor: Color(32), styles: {TermStyle.underline}, ), - ColorToken(text: '!', fgColor: 0, bgColor: 0), + ColorToken(text: '!'), ]; i = 0; diff --git a/lib/src/color.dart b/lib/src/color.dart new file mode 100644 index 0000000..e9dc006 --- /dev/null +++ b/lib/src/color.dart @@ -0,0 +1,56 @@ +class Color { + final dynamic value; + + const Color(this.value); + + static const Color none = Color(0); + + String get formatted => value.toString(); + + @override + String toString() => 'Color($value)'; + + @override + operator ==(Object other) { + if (other is Color) { + return value == other.value; + } + return value == other; + } + + @override + int get hashCode => value.hashCode; +} + +class RGBColor extends Color { + final int red; + final int green; + final int blue; + + const RGBColor(this.red, this.green, this.blue) : super('$red;$green;$blue'); + + @override + String get formatted => '$red;$green;$blue'; + + String get formattedForeground => '38;2;$formatted'; + String get formattedBackground => '48;2;$formatted'; + + @override + String toString() => 'RGBColor($red, $green, $blue)'; +} + +class ANSIColor extends Color { + final int color; + + const ANSIColor(this.color) : super(color); + + bool get isForeground => (color >= 30 && color <= 37) && (color >= 90 && color <= 97); + bool get isBackground => (color >= 40 && color <= 47) && (color >= 100 && color <= 107); + + @override + String get formatted => color.toString(); + + @override + String toString() => 'ANSIColor($color)'; +} + diff --git a/lib/src/parser.dart b/lib/src/parser.dart index fbb7588..83e0556 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -1,3 +1,4 @@ +import 'color.dart'; import 'interfaces.dart'; import 'reader.dart'; import 'consts.dart'; @@ -85,16 +86,10 @@ class ColorParser implements IReader { token.setStyle(color); } else if (_checkBetween(color, 30, 37) || _checkBetween(color, 90, 97)) { - token.fgColor = color; - if (_checkBetween(color, 90, 97)) { - token.brightFg = true; - } + token.fgColor = ANSIColor(color); } else if (_checkBetween(color, 40, 47) || _checkBetween(color, 100, 107)) { - token.bgColor = color; - if (_checkBetween(color, 100, 107)) { - token.brightBg = true; - } + token.bgColor = ANSIColor(color); } else { // Catch arbitrary/unhandled codes token.setStyle(color); @@ -117,13 +112,13 @@ class ColorParser implements IReader { if (colors.length < 5) { return token; } - final rgb = '${colors[2]};${colors[3]};${colors[4]}'; + final r = int.parse(colors[2]); + final g = int.parse(colors[3]); + final b = int.parse(colors[4]); if (colors[0] == '38') { - token.rgbFg = true; - token.rgbFgColor = rgb; + token.fgColor = RGBColor(r, g, b); } else if (colors[0] == '48') { - token.rgbBg = true; - token.rgbBgColor = rgb; + token.bgColor = RGBColor(r, g, b); } return token; } @@ -134,11 +129,9 @@ class ColorParser implements IReader { } final color = int.parse(colors[2]); if (colors[0] == '38') { - token.xterm256 = true; - token.fgColor = color; + token.fgColor = ANSIColor(color); } else if (colors[0] == '48') { - token.xterm256 = true; - token.bgColor = color; + token.bgColor = ANSIColor(color); } return token; } diff --git a/lib/src/token.dart b/lib/src/token.dart index 3fa4a6e..1195896 100644 --- a/lib/src/token.dart +++ b/lib/src/token.dart @@ -1,3 +1,4 @@ +import 'color.dart'; import 'consts.dart'; /// Represents a string value with color information. @@ -10,12 +11,10 @@ class ColorToken { String text; /// The foreground color code. - int fgColor; - String rgbFgColor; + Color fgColor; /// The background color code. - int bgColor; - String rgbBgColor; + Color bgColor; /// Whether the text is bold. bool get bold => styles.contains(TermStyle.bold); @@ -48,53 +47,33 @@ class ColorToken { bool get reset => styles.contains(TermStyle.reset); /// Whether the text has a foreground color. - bool get hasFgColor => fgColor != 0; + bool get hasFgColor => fgColor != Color.none; /// Whether the text has a background color. - bool get hasBgColor => bgColor != 0; - - /// Whether the text is an xterm256 color code. Otherwise, it is an ANSI color code. - bool xterm256; - - /// Whether the text is using r;g;b for foreground - bool rgbFg; - - /// Whether the text is using r;g;b for background - bool rgbBg; - - /// Whether the text is using bright foreground colors - bool brightFg; - - /// Whether the text is using bright background colors - bool brightBg; + bool get hasBgColor => bgColor != Color.none; /// The styles applied to the text. late Set styles; ColorToken({ required this.text, - required this.fgColor, - required this.bgColor, - this.xterm256 = false, - this.rgbFg = false, - this.rgbBg = false, - this.rgbFgColor = "", - this.rgbBgColor = "", - this.brightFg = false, - this.brightBg = false, + this.fgColor = Color.none, + this.bgColor = Color.none, Set? styles, }) : styles = styles ?? {}; /// Create an empty token. - factory ColorToken.empty() => ColorToken(text: '', fgColor: 0, bgColor: 0); + factory ColorToken.empty() => ColorToken(text: ''); /// Create an empty token with a reset style. - factory ColorToken.emptyReset() => - ColorToken(text: '', fgColor: 0, bgColor: 0, styles: {TermStyle.reset}); + factory ColorToken.emptyReset() => ColorToken( + text: '', + styles: {TermStyle.reset}, + ); /// Create a token with default color and the given text. factory ColorToken.fromText(String text) => - ColorToken(text: text, fgColor: 0, bgColor: 0); + ColorToken(text: text, fgColor: Color.none, bgColor: Color.none); /// Returns true if the text is empty. bool get isEmpty => text.isEmpty; @@ -109,47 +88,40 @@ class ColorToken { /// To format the text in other ways, use the properties to get the [fgColor], [bgColor], /// and other [styles], and construct it to the desired output format. String get formatted { - var colorCodes = ''; - if (xterm256) { - colorCodes = '38;5;$fgColor'; - if (bgColor != 0) { - colorCodes += ';48;5;$bgColor'; - } - } else if (rgbFg || rgbBg) { - if (rgbFgColor != "") { - colorCodes = '38;2;$rgbFgColor'; - } - if (rgbBgColor != "") { - colorCodes += ';48;2;$rgbBgColor'; - } - } else { - colorCodes = fgColor == 0 ? '' : '$fgColor'; - if (bgColor != 0) { - colorCodes += ';$bgColor'; - } - } - // final nonResetStyles = styles.where((x) => x != TermStyle.reset).toList(); - final styleCodes = - styles.isNotEmpty ? styles.map((s) => Consts.codeMap[s]).join(';') : ''; + final parts = []; - final tokens = _tokenString( - [colorCodes, styleCodes].where((s) => s.isNotEmpty).join(';')); - // final reset = this.reset ? _tokenString(Consts.resetByte.toString()) : ''; + // foreground + if (fgColor is RGBColor) { + parts.add('38;2;${fgColor.formatted}'); + } else if (fgColor != Color.none) { + parts.add(fgColor.formatted); + } + + // background + if (bgColor is RGBColor) { + parts.add('48;2;${bgColor.formatted}'); + } else if (bgColor != Color.none) { + parts.add(bgColor.formatted); + } + + final styleParts = + styles.map((s) => Consts.codeMap[s]?.toString()).whereType(); + + parts.addAll(styleParts); + + final tokens = _tokenString(parts.where((s) => s.isNotEmpty).join(';')); return '$tokens$text'; } @override - String toString() => 'ColoredText(${debugProperties().join(', ')})'; + String toString() => 'ColorToken(${debugProperties().join(', ')})'; /// Returns a list of debug properties. List debugProperties() => [ 'text: "${_debugString(text)}"', 'fgColor: $fgColor', 'bgColor: $bgColor', - 'xterm256: $xterm256', - 'rgbFG: $rgbFgColor', - 'rgbBG: $rgbBgColor', 'styles: ${styles.map((s) => s.name)}', ]; diff --git a/test/color_parser_test.dart b/test/color_parser_test.dart index 9b76658..27e8330 100644 --- a/test/color_parser_test.dart +++ b/test/color_parser_test.dart @@ -1,3 +1,4 @@ +import 'package:terminal_color_parser/src/color.dart'; import 'package:terminal_color_parser/terminal_color_parser.dart'; import 'package:test/test.dart'; @@ -7,6 +8,7 @@ const inputs = [ '\x1B[0m\x1B[1m\x1B[0m\x1B[1m\x1B[31mWelcome to SimpleMUD\x1B[0m\x1B[1m\x1B[0m', '\x1B[0m\x1B[37m\x1B[0m\x1B[37m\x1B[1m[\x1B[0m\x1B[37m\x1B[1m\x1B[32m10\x1B[0m\x1B[37m\x1B[1m/10]\x1B[0m\x1B[37m\x1B[0m', '\x1B[0m"If you are ready to advance, young fellow, you may \x1B[1m\x1B[33mtrain\x1B[0m here."\x1B[0m\x1B[1m\x1B[0m', + '\x1B[38;2;255;0;0mRed\x1B[0m', ]; void main() { @@ -17,53 +19,57 @@ void main() { expect(output, [ ColorToken( text: 'You are standing in a small clearing.', - fgColor: 32, - bgColor: 0, + fgColor: ANSIColor(32), styles: {}, // styles: {TermStyle.reset}, ), ColorToken.emptyReset(), ]); }); + test('parse colors - simple colors 2', () { final input = inputs[4]; final output = ColorParser(input).parse(); expect(output, [ ColorToken( text: '"If you are ready to advance, young fellow, you may ', - fgColor: 0, - bgColor: 0, styles: {TermStyle.reset}, ), ColorToken( text: 'train', - fgColor: 33, - bgColor: 0, + fgColor: ANSIColor(33), styles: {TermStyle.bold}, ), ColorToken( text: ' here."', - fgColor: 0, - bgColor: 0, styles: {TermStyle.reset}, ), ColorToken( text: '', - fgColor: 0, - bgColor: 0, styles: {TermStyle.reset, TermStyle.bold}, ), ]); }); + test('parse colors - rgb', () { + final input = inputs[5]; + final output = ColorParser(input).parse(); + expect(output, [ + ColorToken( + text: 'Red', + styles: {}, + fgColor: RGBColor(255, 0, 0), + ), + ColorToken.emptyReset(), + ]); + }); + test('parse colors - no colors', () { final input = inputs[1]; final output = ColorParser(input).parse(); expect(output, [ ColorToken( text: 'You are standing in a small clearing.', - fgColor: 0, - bgColor: 0, styles: {}, ), ]); @@ -77,3 +83,4 @@ void main() { }); }); } +