feat!: add color classes

This commit is contained in:
2025-01-11 00:05:27 +02:00
parent 1784a11b23
commit ae0364d063
6 changed files with 126 additions and 98 deletions

View File

@@ -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) {

View File

@@ -1,10 +1,11 @@
import 'package:terminal_color_parser/src/color.dart';
import 'package:terminal_color_parser/terminal_color_parser.dart';
void main(List<String> 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<String> 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;

56
lib/src/color.dart Normal file
View File

@@ -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)';
}

View File

@@ -1,3 +1,4 @@
import 'color.dart';
import 'interfaces.dart';
import 'reader.dart';
import 'consts.dart';
@@ -85,16 +86,10 @@ class ColorParser implements IReader<StringTokenValue> {
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<StringTokenValue> {
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<StringTokenValue> {
}
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;
}

View File

@@ -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<TermStyle> 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<TermStyle>? 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 = <String>[];
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<String>();
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<String> debugProperties() => [
'text: "${_debugString(text)}"',
'fgColor: $fgColor',
'bgColor: $bgColor',
'xterm256: $xterm256',
'rgbFG: $rgbFgColor',
'rgbBG: $rgbBgColor',
'styles: ${styles.map((s) => s.name)}',
];

View File

@@ -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() {
});
});
}