Color parser (#1)

* wip: color parser

* feat: better color parsing

* feat: color parser working
This commit is contained in:
Chen Asraf
2023-09-22 00:12:23 +03:00
committed by GitHub
parent 9dc2e057b2
commit 5fe22d93b7
9 changed files with 641 additions and 38 deletions

View File

@@ -1,22 +1,361 @@
import 'package:flutter/foundation.dart';
import 'package:mudblock/core/consts.dart';
import 'parser/parser.dart';
class ColorUtils {
static stripColor(String text) {
return text
// esc
// .replaceAll(esc, '')
// .replaceAll(r'^[', '')
// color
.replaceAll(RegExp(esc + r'\[\d*m'), '')
// esc
// .replaceAll(esc, '')
// .replaceAll(r'^[', '')
// color
.replaceAll(RegExp(esc + colorPatternRaw), '')
// color
// .replaceAll(RegExp(r'\[\d+m'), '')
// esc
// .replaceAll(String.fromCharCode(0xff), '');
// color
// .replaceAll(RegExp(r'\[\d+m'), '')
// esc
// .replaceAll(String.fromCharCode(0xff), '');
//
;
//
;
}
static Iterable<ColoredText> split(String line) {
// return line.split(RegExp(esc + colorPatternRaw)).map(
// (raw) => ColoredText(
// text: stripColor(raw),
// color: 0,
// raw: raw,
// ),
// );
try {
final result = <ColoredText>[];
// final tokenizer = Tokenizer(StringReader(line));
// var lexer = Lexer(tokenizer);
// final tokens = lexer.lex();
// for (var i = 0; i < tokens.length; i++) {
// final token = tokens[i];
// result.add(
// ColoredText(
// text: token.text,
// fgColor: token.fgColor,
// bgColor: token.bgColor,
// raw: token.text,
// ),
// );
// }
final tokens = ColorParser(line).parse();
for (final token in tokens) {
result.add(
ColoredText(
text: token.text,
fgColor: token.fgColor,
bgColor: token.bgColor,
raw: token.text,
),
);
}
debugPrint('line: $line');
debugPrint('split: $result');
return result;
} catch (e, stack) {
debugPrint('error at line: $line');
debugPrint('split error: $e $stack');
return [
ColoredText(
text: line,
fgColor: 0,
bgColor: 0,
raw: line,
)
];
}
}
}
class ColoredText {
final String text;
final int fgColor;
final int bgColor;
final String raw;
ColoredText({
required this.text,
required this.fgColor,
required this.bgColor,
required this.raw,
});
int get themedFgColor => _colorMap[fgColor] ?? fgColor;
int get themedBgColor => _colorMap[bgColor] ?? bgColor;
@override
String toString() => 'ColoredText($text, $fgColor, $bgColor, $raw)';
}
/// map of xterm 256 colors to flutter color ints
const _colorMap = {
// 0: 0xFF000000,
0: 0xFFFFFFFF,
1: 0xFF800000,
2: 0xFF008000,
3: 0xFF808000,
4: 0xFF000080,
5: 0xFF800080,
6: 0xFF008080,
7: 0xFFC0C0C0,
8: 0xFF808080,
9: 0xFFFF0000,
10: 0xFF00FF00,
11: 0xFFFFFF00,
12: 0xFF0000FF,
13: 0xFFFF00FF,
14: 0xFF00FFFF,
15: 0xFFFFFFFF,
16: 0xFF000000,
17: 0xFF00005F,
18: 0xFF000087,
19: 0xFF0000AF,
20: 0xFF0000D7,
21: 0xFF0000FF,
22: 0xFF005F00,
23: 0xFF005F5F,
24: 0xFF005F87,
25: 0xFF005FAF,
26: 0xFF005FD7,
27: 0xFF005FFF,
28: 0xFF008700,
29: 0xFF00875F,
30: 0xFF008787,
31: 0xFF0087AF,
32: 0xFF0087D7,
33: 0xFF0087FF,
34: 0xFF00AF00,
35: 0xFF00AF5F,
36: 0xFF00AF87,
37: 0xFF00AFAF,
38: 0xFF00AFD7,
39: 0xFF00AFFF,
40: 0xFF00D700,
41: 0xFF00D75F,
42: 0xFF00D787,
43: 0xFF00D7AF,
44: 0xFF00D7D7,
45: 0xFF00D7FF,
46: 0xFF00FF00,
47: 0xFF00FF5F,
48: 0xFF00FF87,
49: 0xFF00FFAF,
50: 0xFF00FFD7,
51: 0xFF00FFFF,
52: 0xFF5F0000,
53: 0xFF5F005F,
54: 0xFF5F0087,
55: 0xFF5F00AF,
56: 0xFF5F00D7,
57: 0xFF5F00FF,
58: 0xFF5F5F00,
59: 0xFF5F5F5F,
60: 0xFF5F5F87,
61: 0xFF5F5FAF,
62: 0xFF5F5FD7,
63: 0xFF5F5FFF,
64: 0xFF5F8700,
65: 0xFF5F875F,
66: 0xFF5F8787,
67: 0xFF5F87AF,
68: 0xFF5F87D7,
69: 0xFF5F87FF,
70: 0xFF5FAF00,
71: 0xFF5FAF5F,
72: 0xFF5FAF87,
73: 0xFF5FAFAF,
74: 0xFF5FAFD7,
75: 0xFF5FAFFF,
76: 0xFF5FD700,
77: 0xFF5FD75F,
78: 0xFF5FD787,
79: 0xFF5FD7AF,
80: 0xFF5FD7D7,
81: 0xFF5FD7FF,
82: 0xFF5FFF00,
83: 0xFF5FFF5F,
84: 0xFF5FFF87,
85: 0xFF5FFFAF,
86: 0xFF5FFFD7,
87: 0xFF5FFFFF,
88: 0xFF870000,
89: 0xFF87005F,
90: 0xFF870087,
91: 0xFF8700AF,
92: 0xFF8700D7,
93: 0xFF8700FF,
94: 0xFF875F00,
95: 0xFF875F5F,
96: 0xFF875F87,
97: 0xFF875FAF,
98: 0xFF875FD7,
99: 0xFF875FFF,
100: 0xFF878700,
101: 0xFF87875F,
102: 0xFF878787,
103: 0xFF8787AF,
104: 0xFF8787D7,
105: 0xFF8787FF,
106: 0xFF87AF00,
107: 0xFF87AF5F,
108: 0xFF87AF87,
109: 0xFF87AFAF,
110: 0xFF87AFD7,
111: 0xFF87AFFF,
112: 0xFF87D700,
113: 0xFF87D75F,
114: 0xFF87D787,
115: 0xFF87D7AF,
116: 0xFF87D7D7,
117: 0xFF87D7FF,
118: 0xFF87FF00,
119: 0xFF87FF5F,
120: 0xFF87FF87,
121: 0xFF87FFAF,
122: 0xFF87FFD7,
123: 0xFF87FFFF,
124: 0xFFAF0000,
125: 0xFFAF005F,
126: 0xFFAF0087,
127: 0xFFAF00AF,
128: 0xFFAF00D7,
129: 0xFFAF00FF,
130: 0xFFAF5F00,
131: 0xFFAF5F5F,
132: 0xFFAF5F87,
133: 0xFFAF5FAF,
134: 0xFFAF5FD7,
135: 0xFFAF5FFF,
136: 0xFFAF8700,
137: 0xFFAF875F,
138: 0xFFAF8787,
139: 0xFFAF87AF,
140: 0xFFAF87D7,
141: 0xFFAF87FF,
142: 0xFFAFAF00,
143: 0xFFAFAF5F,
144: 0xFFAFAF87,
145: 0xFFAFAFAF,
146: 0xFFAFAFD7,
147: 0xFFAFAFFF,
148: 0xFFAFD700,
149: 0xFFAFD75F,
150: 0xFFAFD787,
151: 0xFFAFD7AF,
152: 0xFFAFD7D7,
153: 0xFFAFD7FF,
154: 0xFFAFFF00,
155: 0xFFAFFF5F,
156: 0xFFAFFF87,
157: 0xFFAFFFAF,
158: 0xFFAFFFD7,
159: 0xFFAFFFFF,
160: 0xFFD70000,
161: 0xFFD7005F,
162: 0xFFD70087,
163: 0xFFD700AF,
164: 0xFFD700D7,
165: 0xFFD700FF,
166: 0xFFD75F00,
167: 0xFFD75F5F,
168: 0xFFD75F87,
169: 0xFFD75FAF,
170: 0xFFD75FD7,
171: 0xFFD75FFF,
172: 0xFFD78700,
173: 0xFFD7875F,
174: 0xFFD78787,
175: 0xFFD787AF,
176: 0xFFD787D7,
177: 0xFFD787FF,
178: 0xFFD7AF00,
179: 0xFFD7AF5F,
180: 0xFFD7AF87,
181: 0xFFD7AFAF,
182: 0xFFD7AFD7,
183: 0xFFD7AFFF,
184: 0xFFD7D700,
185: 0xFFD7D75F,
186: 0xFFD7D787,
187: 0xFFD7D7AF,
188: 0xFFD7D7D7,
189: 0xFFD7D7FF,
190: 0xFFD7FF00,
191: 0xFFD7FF5F,
192: 0xFFD7FF87,
193: 0xFFD7FFAF,
194: 0xFFD7FFD7,
195: 0xFFD7FFFF,
196: 0xFFFF0000,
197: 0xFFFF005F,
198: 0xFFFF0087,
199: 0xFFFF00AF,
200: 0xFFFF00D7,
201: 0xFFFF00FF,
202: 0xFFFF5F00,
203: 0xFFFF5F5F,
204: 0xFFFF5F87,
205: 0xFFFF5FAF,
206: 0xFFFF5FD7,
207: 0xFFFF5FFF,
208: 0xFFFF8700,
209: 0xFFFF875F,
210: 0xFFFF8787,
211: 0xFFFF87AF,
212: 0xFFFF87D7,
213: 0xFFFF87FF,
214: 0xFFFFAF00,
215: 0xFFFFAF5F,
216: 0xFFFFAF87,
217: 0xFFFFAFAF,
218: 0xFFFFAFD7,
219: 0xFFFFAFFF,
220: 0xFFFFD700,
221: 0xFFFFD75F,
222: 0xFFFFD787,
223: 0xFFFFD7AF,
224: 0xFFFFD7D7,
225: 0xFFFFD7FF,
226: 0xFFFFFF00,
227: 0xFFFFFF5F,
228: 0xFFFFFF87,
229: 0xFFFFFFAF,
230: 0xFFFFFFD7,
231: 0xFFFFFFFF,
232: 0xFF080808,
233: 0xFF121212,
234: 0xFF1C1C1C,
235: 0xFF262626,
236: 0xFF303030,
237: 0xFF3A3A3A,
238: 0xFF444444,
239: 0xFF4E4E4E,
240: 0xFF585858,
241: 0xFF626262,
242: 0xFF6C6C6C,
243: 0xFF767676,
244: 0xFF808080,
245: 0xFF8A8A8A,
246: 0xFF949494,
247: 0xFF9E9E9E,
248: 0xFFA8A8A8,
249: 0xFFB2B2B2,
250: 0xFFBCBCBC,
251: 0xFFC6C6C6,
252: 0xFFD0D0D0,
253: 0xFFDADADA,
254: 0xFFE4E4E4,
255: 0xFFEEEEEE,
};

View File

@@ -2,3 +2,4 @@ const newline = '\n';
// const ansiEscapePattern = r'\x1B\[[0-?]*[ -/]*[@-~]';
// final esc = String.fromCharCodes([0xff]);
const esc = '\x1B';
const colorPatternRaw = r'\[\d*m';

View File

@@ -0,0 +1,74 @@
import '../consts.dart' as consts;
enum Token {
esc,
colorStart,
colorTerm,
colorSeparator,
literal,
}
class TokenValue {
final Token token;
final String raw;
const TokenValue(this.token, this.raw);
static const esc = TokenValue(Token.esc, consts.esc);
static const colorStart = TokenValue(Token.colorStart, '[');
static const colorSeparator = TokenValue(Token.colorSeparator, ';');
static const colorTerm = TokenValue(Token.colorTerm, 'm');
static const empty = TokenValue(Token.literal, '');
TokenValue.literal(String raw) : this(Token.literal, raw);
@override
int get hashCode => raw.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TokenValue && runtimeType == other.runtimeType && token == other.token && raw == other.raw;
@override
String toString() => token != Token.esc ? '${token.name}($raw)' : token.name;
}
class LexerValue {
String text;
int fgColor;
int bgColor;
LexerValue(this.text, this.fgColor, this.bgColor);
@override
int get hashCode => text.hashCode ^ fgColor.hashCode ^ bgColor.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is LexerValue &&
runtimeType == other.runtimeType &&
text == other.text &&
fgColor == other.fgColor &&
bgColor == other.bgColor;
@override
String toString() => 'Lex("$text", $fgColor:$bgColor)';
}
abstract class IReader<T> {
T? read();
T? peek();
int get length;
int get index;
bool get isDone;
}
abstract class ITokenizer {
List<TokenValue> tokenize();
}
abstract class ILexer {
List<LexerValue> lex();
}

123
lib/core/parser/parser.dart Normal file
View File

@@ -0,0 +1,123 @@
import 'interfaces.dart';
import 'reader.dart';
import '../consts.dart' as consts;
class ColorToken {
String text;
int fgColor;
int bgColor;
ColorToken(this.text, this.fgColor, this.bgColor);
factory ColorToken.empty() => ColorToken('', 0, 0);
bool get isEmpty => text.isEmpty;
bool get isNotEmpty => !isEmpty;
@override
String toString() => 'ColorToken("$text", $fgColor:$bgColor)';
@override
int get hashCode => text.hashCode ^ fgColor.hashCode ^ bgColor.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ColorToken &&
runtimeType == other.runtimeType &&
text == other.text &&
fgColor == other.fgColor &&
bgColor == other.bgColor;
}
class ColorParser implements IReader {
final IReader reader;
final _tokens = <TokenValue>[];
ColorParser._(this.reader);
factory ColorParser(String text) => ColorParser._(StringReader(text));
List<ColorToken> parse() {
final lexed = <ColorToken>[];
while (!reader.isDone) {
final token = reader.read();
var cur = getToken(token);
lexed.add(cur);
}
return lexed;
}
ColorToken getToken(String char) {
var token = ColorToken.empty();
switch (char) {
case consts.esc:
String? next;
// keep reading until we hit the end of the escape sequence or the end of the string
while (!reader.isDone) {
next = reader.peek();
if (next == consts.esc) {
break;
}
reader.read();
if (next == '[') {
final color = consumeUntil('m');
reader.read();
final colors = color.split(';');
if (colors.isNotEmpty) {
token.fgColor = int.tryParse(colors[0]) ?? 0;
}
if (colors.length == 2) {
token.bgColor = int.tryParse(colors[1]) ?? 0;
}
token.text = consumeUntil(consts.esc);
return token;
}
if (next == null) {
break;
}
token.text += next;
}
return token;
default:
token.text += char;
return token;
}
}
String consumeUntil(String char) {
String? next = reader.peek();
if (next == null) {
return '';
}
var result = '';
while (!reader.isDone) {
if (next == char) {
break;
}
next = reader.peek();
if (next == null) {
break;
}
result += reader.read();
next = reader.peek();
}
return result;
}
@override
int index = 0;
@override
bool get isDone => index >= reader.length;
@override
peek() => _tokens[index];
@override
read() => _tokens[index++];
@override
int get length => _tokens.length;
}

View File

@@ -0,0 +1,33 @@
import 'interfaces.dart';
class StringReader implements IReader<String> {
final String input;
@override
int index = 0;
StringReader(this.input);
@override
int get length => input.length;
@override
bool get isDone => index >= length;
@override
String? peek() {
if (isDone) {
return null;
}
return input[index];
}
@override
String? read() {
if (isDone) {
return null;
}
return input[index++];
}
}

View File

@@ -55,6 +55,7 @@ class GameStore extends ChangeNotifier {
}
void onLine(String line) {
debugPrint('onLine: $line');
addLine(line);
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:mudblock/core/color_utils.dart';
import 'package:provider/provider.dart';
import '../core/consts.dart';
import '../core/store.dart';
class HomePage extends StatefulWidget {
@@ -22,6 +23,7 @@ class _HomePageState extends State<HomePage> with GameStoreMixin {
final inputStyle = consoleStyle.copyWith(color: Colors.grey);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Material(
@@ -29,22 +31,28 @@ class _HomePageState extends State<HomePage> with GameStoreMixin {
child: Consumer<GameStore>(
builder: (context, store, child) {
final lines = store.lines;
return ListView.builder(
return SingleChildScrollView(
controller: store.scrollController,
shrinkWrap: true,
itemBuilder: (context, index) {
return RichText(
text: TextSpan(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SelectableText.rich(
TextSpan(
children: [
TextSpan(
text: ColorUtils.stripColor(lines[index]),
style: consoleStyle,
),
for (final line in lines) ...[
for (final segment in ColorUtils.split(line))
TextSpan(
text: segment.text,
style: consoleStyle.copyWith(color: Color(segment.themedFgColor)),
),
const TextSpan(
text: newline,
style: consoleStyle,
),
],
],
),
);
},
itemCount: lines.length,
),
),
);
},
),

View File

@@ -0,0 +1,24 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mudblock/core/consts.dart';
import 'package:mudblock/core/parser/parser.dart';
const inputs = [
'$esc[32mYou are standing in a small clearing.$esc[0m',
'You are standing in a small clearing.',
'$esc[0m$esc[1m$esc[0m$esc[1m$esc[31mWelcome to SimpleMUD$esc[0m$esc[1m$esc[0m',
'$esc[0m$esc[37m$esc[0m$esc[37m$esc[1m[$esc[0m$esc[37m$esc[1m$esc[32m10$esc[0m$esc[37m$esc[1m/10]$esc[0m$esc[37m$esc[0m'
];
void main() {
group('ColorParser', () {
test('colored output 1', () {
final input = inputs[0];
final output = ColorParser(input).parse();
expect(output, [
ColorToken('You are standing in a small clearing.', 32, 0),
ColorToken.empty(),
]);
});
});
}

View File

@@ -11,20 +11,20 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mudblock/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
// await tester.pumpWidget(const MyApp());
//
// // Verify that our counter starts at 0.
// expect(find.text('0'), findsOneWidget);
// expect(find.text('1'), findsNothing);
//
// // Tap the '+' icon and trigger a frame.
// await tester.tap(find.byIcon(Icons.add));
// await tester.pump();
//
// // Verify that our counter has incremented.
// expect(find.text('0'), findsNothing);
// expect(find.text('1'), findsOneWidget);
// });
}