Isovel/rtf color parsing (#27)

* feat: improve RTF color parsing to be more consistent with Resonite

* chore: optimize some regex used in RTF tag parsing

---------

Co-authored-by: Garrett Watson <toast@isota.ch>
This commit is contained in:
Nutcake 2024-01-27 16:15:27 +01:00 committed by GitHub
parent 76e32887e4
commit cc92786af2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 101 additions and 42 deletions

View file

@ -164,7 +164,7 @@ class _ReConState extends State<ReCon> {
}, },
child: DynamicColorBuilder( child: DynamicColorBuilder(
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp( builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: true,
title: 'ReCon', title: 'ReCon',
theme: ThemeData( theme: ThemeData(
useMaterial3: true, useMaterial3: true,

View file

@ -40,9 +40,7 @@ class FormatNode {
} }
} }
if (substr.isNotEmpty) { if (substr.isNotEmpty) {
root.children.add( root.children.add(FormatNode.buildFromStyles(activeTags, substr));
FormatNode.buildFromStyles(activeTags, substr)
);
} }
} }
return root; return root;
@ -50,9 +48,9 @@ class FormatNode {
TextSpan toTextSpan({required TextStyle baseStyle}) { TextSpan toTextSpan({required TextStyle baseStyle}) {
final spanTree = TextSpan( final spanTree = TextSpan(
text: text, text: text,
style: format.isUnformatted ? baseStyle : format.style(), style: format.isUnformatted ? baseStyle : format.style(),
children: children.map((e) => e.toTextSpan(baseStyle: baseStyle)).toList() children: children.map((e) => e.toTextSpan(baseStyle: baseStyle)).toList(),
); );
return spanTree; return spanTree;
} }
@ -87,8 +85,10 @@ class FormatTag {
required this.format, required this.format,
}); });
static final _tagRegExp = RegExp(r"<(.+?)>");
static List<FormatTag> parseTags(String text) { static List<FormatTag> parseTags(String text) {
final startMatches = RegExp(r"<(.+?)>").allMatches(text); final startMatches = _tagRegExp.allMatches(text);
final spans = <FormatTag>[]; final spans = <FormatTag>[];
@ -96,13 +96,11 @@ class FormatTag {
final fullTag = startMatch.group(1); final fullTag = startMatch.group(1);
if (fullTag == null) continue; if (fullTag == null) continue;
final tag = FormatData.parse(fullTag); final tag = FormatData.parse(fullTag);
spans.add( spans.add(FormatTag(
FormatTag( startIndex: startMatch.start,
startIndex: startMatch.start, endIndex: startMatch.end,
endIndex: startMatch.end, format: tag,
format: tag, ));
)
);
} }
return spans; return spans;
} }
@ -116,20 +114,79 @@ class FormatAction {
} }
class FormatData { class FormatData {
static Color? tryParseColor(String? text) { static final Map<String, Map<String, Color>> _platformColorPalette = {
if (text == null) return null; "neutrals": {
var color = cc.RgbColor.namedColors[text]; "dark": const Color(0xFF11151D),
if (color != null) { "mid": const Color(0xFF86888B),
return Color.fromARGB(255, color.r.round(), color.g.round(), color.b.round()); "light": const Color(0xFFE1E1E0),
} },
"hero": {
"yellow": const Color(0xFFF8F770),
"green": const Color(0xFF59EB5C),
"red": const Color(0xFFFF7676),
"purple": const Color(0xFFBA64F2),
"cyan": const Color(0xFF61D1FA),
"orange": const Color(0xFFE69E50),
},
"sub": {
"yellow": const Color(0xFF484A2C),
"green": const Color(0xFF24512C),
"red": const Color(0xFF5D323A),
"purple": const Color(0xFF492F64),
"cyan": const Color(0xFF284C5D),
"orange": const Color(0xFF48392A),
},
"dark": {
"yellow": const Color(0xFF2B2E26),
"green": const Color(0xFF192D24),
"red": const Color(0xFF1A1318),
"purple": const Color(0xFF241E35),
"cyan": const Color(0xFF1A2A36),
"orange": const Color(0xFF292423),
},
};
static final _hexColorRegExp = RegExp(r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$");
static final _platformColorRegExp = RegExp(r"^([a-zA-Z]+)\.([a-zA-Z]+)$");
static Color? _parseHexColor(String text) {
try { try {
color = cc.HexColor(text); if (text.startsWith("#")) text = text.substring(1);
if (text.length == 3) text = text.split("").map((e) => e + e).join("");
final color = cc.HexColor(text);
return Color.fromARGB(255, color.r.round(), color.g.round(), color.b.round()); return Color.fromARGB(255, color.r.round(), color.g.round(), color.b.round());
} catch (_) { } catch (_) {
return null; return null;
} }
} }
static Color? tryParseColor(String text) {
// is it a hex color?
if (_hexColorRegExp.hasMatch(text)) {
return _parseHexColor(text);
}
// is it one of Resonite's color constants?
if (_platformColorRegExp.hasMatch(text)) {
final parts = text.split(".");
if (parts.length == 2) {
final palette = _platformColorPalette[parts[0]];
if (palette != null) {
return palette[parts[1]];
}
}
}
// is it a named color?
final color = cc.RgbColor.namedColors[text];
if (color != null) {
return Color.fromARGB(255, color.r.round(), color.g.round(), color.b.round());
}
// whatever it is, it's probably safe to assume it's not a color
return null;
}
static final Map<String, FormatAction> _richTextTags = { static final Map<String, FormatAction> _richTextTags = {
"align": FormatAction(), "align": FormatAction(),
"alpha": FormatAction(style: (param, baseStyle) { "alpha": FormatAction(style: (param, baseStyle) {
@ -153,8 +210,12 @@ class FormatData {
"line-height": FormatAction(), "line-height": FormatAction(),
"line-indent": FormatAction(), "line-indent": FormatAction(),
"link": FormatAction(), "link": FormatAction(),
"lowercase": FormatAction(transform: (input, parameter) => input.toLowerCase(),), "lowercase": FormatAction(
"uppercase": FormatAction(transform: (input, parameter) => input.toUpperCase(),), transform: (input, parameter) => input.toLowerCase(),
),
"uppercase": FormatAction(
transform: (input, parameter) => input.toUpperCase(),
),
"smallcaps": FormatAction(), "smallcaps": FormatAction(),
"margin": FormatAction(), "margin": FormatAction(),
"mark": FormatAction(style: (param, baseStyle) { "mark": FormatAction(style: (param, baseStyle) {
@ -168,22 +229,20 @@ class FormatData {
"nobr": FormatAction(), "nobr": FormatAction(),
"page": FormatAction(), "page": FormatAction(),
"pos": FormatAction(), "pos": FormatAction(),
"size": FormatAction( "size": FormatAction(style: (param, baseStyle) {
style: (param, baseStyle) { if (param == null) return baseStyle;
if (param == null) return baseStyle; final baseSize = baseStyle.fontSize ?? 12;
final baseSize = baseStyle.fontSize ?? 12; if (param.endsWith("%")) {
if (param.endsWith("%")) { final percentage = int.tryParse(param.replaceAll("%", ""));
final percentage = int.tryParse(param.replaceAll("%", "")); if (percentage == null || percentage <= 0) return baseStyle;
if (percentage == null || percentage <= 0) return baseStyle; return baseStyle.copyWith(fontSize: baseSize * (percentage / 100));
return baseStyle.copyWith(fontSize: baseSize * (percentage / 100)); } else {
} else { final size = num.tryParse(param);
final size = num.tryParse(param); if (size == null || size <= 0) return baseStyle;
if (size == null || size <= 0) return baseStyle; final realSize = baseSize * (size / 1000);
final realSize = baseSize * (size / 1000); return baseStyle.copyWith(fontSize: realSize.toDouble().clamp(8, 400));
return baseStyle.copyWith(fontSize: realSize.toDouble().clamp(8, 400)); }
} }),
}
),
"space": FormatAction(), "space": FormatAction(),
"sprite": FormatAction(), "sprite": FormatAction(),
"s": FormatAction(style: (param, baseStyle) => baseStyle.copyWith(decoration: TextDecoration.lineThrough)), "s": FormatAction(style: (param, baseStyle) => baseStyle.copyWith(decoration: TextDecoration.lineThrough)),

View file

@ -109,4 +109,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 3efd3b4b57928fa6a5be6b71a1f5dc6e2a2b54af PODFILE CHECKSUM: 3efd3b4b57928fa6a5be6b71a1f5dc6e2a2b54af
COCOAPODS: 1.12.1 COCOAPODS: 1.14.3