From cc92786af2ad7ef1f279f5f94ba8d95bf4533d24 Mon Sep 17 00:00:00 2001 From: Nutcake Date: Sat, 27 Jan 2024 16:15:27 +0100 Subject: [PATCH] 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 --- lib/main.dart | 2 +- lib/string_formatter.dart | 139 +++++++++++++++++++++++++++----------- macos/Podfile.lock | 2 +- 3 files changed, 101 insertions(+), 42 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 8de17ae..897c1a2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -164,7 +164,7 @@ class _ReConState extends State { }, child: DynamicColorBuilder( builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp( - debugShowCheckedModeBanner: false, + debugShowCheckedModeBanner: true, title: 'ReCon', theme: ThemeData( useMaterial3: true, diff --git a/lib/string_formatter.dart b/lib/string_formatter.dart index e853894..2d76639 100644 --- a/lib/string_formatter.dart +++ b/lib/string_formatter.dart @@ -40,9 +40,7 @@ class FormatNode { } } if (substr.isNotEmpty) { - root.children.add( - FormatNode.buildFromStyles(activeTags, substr) - ); + root.children.add(FormatNode.buildFromStyles(activeTags, substr)); } } return root; @@ -50,9 +48,9 @@ class FormatNode { TextSpan toTextSpan({required TextStyle baseStyle}) { final spanTree = TextSpan( - text: text, - style: format.isUnformatted ? baseStyle : format.style(), - children: children.map((e) => e.toTextSpan(baseStyle: baseStyle)).toList() + text: text, + style: format.isUnformatted ? baseStyle : format.style(), + children: children.map((e) => e.toTextSpan(baseStyle: baseStyle)).toList(), ); return spanTree; } @@ -87,8 +85,10 @@ class FormatTag { required this.format, }); + static final _tagRegExp = RegExp(r"<(.+?)>"); + static List parseTags(String text) { - final startMatches = RegExp(r"<(.+?)>").allMatches(text); + final startMatches = _tagRegExp.allMatches(text); final spans = []; @@ -96,13 +96,11 @@ class FormatTag { final fullTag = startMatch.group(1); if (fullTag == null) continue; final tag = FormatData.parse(fullTag); - spans.add( - FormatTag( - startIndex: startMatch.start, - endIndex: startMatch.end, - format: tag, - ) - ); + spans.add(FormatTag( + startIndex: startMatch.start, + endIndex: startMatch.end, + format: tag, + )); } return spans; } @@ -116,20 +114,79 @@ class FormatAction { } class FormatData { - static Color? tryParseColor(String? text) { - if (text == null) return null; - var color = cc.RgbColor.namedColors[text]; - if (color != null) { - return Color.fromARGB(255, color.r.round(), color.g.round(), color.b.round()); - } + static final Map> _platformColorPalette = { + "neutrals": { + "dark": const Color(0xFF11151D), + "mid": const Color(0xFF86888B), + "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 { - 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()); } catch (_) { 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 _richTextTags = { "align": FormatAction(), "alpha": FormatAction(style: (param, baseStyle) { @@ -153,8 +210,12 @@ class FormatData { "line-height": FormatAction(), "line-indent": FormatAction(), "link": FormatAction(), - "lowercase": FormatAction(transform: (input, parameter) => input.toLowerCase(),), - "uppercase": FormatAction(transform: (input, parameter) => input.toUpperCase(),), + "lowercase": FormatAction( + transform: (input, parameter) => input.toLowerCase(), + ), + "uppercase": FormatAction( + transform: (input, parameter) => input.toUpperCase(), + ), "smallcaps": FormatAction(), "margin": FormatAction(), "mark": FormatAction(style: (param, baseStyle) { @@ -168,22 +229,20 @@ class FormatData { "nobr": FormatAction(), "page": FormatAction(), "pos": FormatAction(), - "size": FormatAction( - style: (param, baseStyle) { - if (param == null) return baseStyle; - final baseSize = baseStyle.fontSize ?? 12; - if (param.endsWith("%")) { - final percentage = int.tryParse(param.replaceAll("%", "")); - if (percentage == null || percentage <= 0) return baseStyle; - return baseStyle.copyWith(fontSize: baseSize * (percentage / 100)); - } else { - final size = num.tryParse(param); - if (size == null || size <= 0) return baseStyle; - final realSize = baseSize * (size / 1000); - return baseStyle.copyWith(fontSize: realSize.toDouble().clamp(8, 400)); - } - } - ), + "size": FormatAction(style: (param, baseStyle) { + if (param == null) return baseStyle; + final baseSize = baseStyle.fontSize ?? 12; + if (param.endsWith("%")) { + final percentage = int.tryParse(param.replaceAll("%", "")); + if (percentage == null || percentage <= 0) return baseStyle; + return baseStyle.copyWith(fontSize: baseSize * (percentage / 100)); + } else { + final size = num.tryParse(param); + if (size == null || size <= 0) return baseStyle; + final realSize = baseSize * (size / 1000); + return baseStyle.copyWith(fontSize: realSize.toDouble().clamp(8, 400)); + } + }), "space": FormatAction(), "sprite": FormatAction(), "s": FormatAction(style: (param, baseStyle) => baseStyle.copyWith(decoration: TextDecoration.lineThrough)), @@ -224,4 +283,4 @@ class FormatData { String? apply(String? text) => text == null ? null : _richTextTags[name]?.transform?.call(text, parameter); TextStyle style() => _richTextTags[name]?.style?.call(parameter, const TextStyle()) ?? const TextStyle(); -} \ No newline at end of file +} diff --git a/macos/Podfile.lock b/macos/Podfile.lock index ee6bac5..33dfd00 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -109,4 +109,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 3efd3b4b57928fa6a5be6b71a1f5dc6e2a2b54af -COCOAPODS: 1.12.1 +COCOAPODS: 1.14.3