diff --git a/lib/model/katex.dart b/lib/model/katex.dart index 2a39e0019b..8a793f663a 100644 --- a/lib/model/katex.dart +++ b/lib/model/katex.dart @@ -1,4 +1,5 @@ import 'package:collection/collection.dart'; +import 'package:convert/convert.dart'; import 'package:csslib/parser.dart' as css_parser; import 'package:csslib/visitor.dart' as css_visitor; import 'package:flutter/foundation.dart'; @@ -629,6 +630,7 @@ class _KatexParser { topEm: _takeStyleEm(inlineStyles, 'top'), marginLeftEm: _takeStyleEm(inlineStyles, 'margin-left'), marginRightEm: _takeStyleEm(inlineStyles, 'margin-right'), + color: _takeStyleColor(inlineStyles, 'color'), // TODO handle more CSS properties ); if (inlineStyles != null && inlineStyles.isNotEmpty) { @@ -715,6 +717,54 @@ class _KatexParser { _hasError = true; return null; } + + /// Remove the given property from the given style map, + /// and parse as a color value. + /// + /// If the property is present but is not a valid CSS Hex color, + /// or is not one of the CSS named color, record an error + /// and return null. + /// + /// If the property is absent, return null with no error. + /// + /// If the map is null, treat it as empty. + /// + /// To produce the map this method expects, see [_parseInlineStyles]. + KatexSpanColor? _takeStyleColor(Map? styles, String property) { + final expression = styles?.remove(property); + if (expression == null) return null; + + // `package:csslib` parser emits a HexColorTerm for the `color` + // attribute. It automatically resolves the named CSS colors to + // their hex values. The `HexColorTerm.value` is the hex + // encoded in an integer in the same sequence as the input hex + // string. But it also allows some non-conformant CSS hex color + // notations, like #f, #ff, #fffff, #fffffff. + // See: + // https://drafts.csswg.org/css-color/#hex-notation. + // https://github.com/dart-lang/tools/blob/2a2a2d611/pkgs/csslib/lib/parser.dart#L2714-L2743 + // + // So, we try to parse the value of `color` attribute ourselves + // only allowing conformant CSS hex color notations, mapping + // named CSS colors to their corresponding values, generating a + // typed result (KatexSpanColor(r, g, b, a)) to be used later + // while rendering. + final valueStr = expression.span?.text; + if (valueStr != null) { + if (valueStr.startsWith('#')) { + final color = parseCssHexColor(valueStr); + if (color != null) return color; + } else { + final color = _cssNamedColorsMap[valueStr]; + if (color != null) return color; + } + } + assert(debugLog('KaTeX: Unsupported value for CSS property $property,' + ' expected a color: ${expression.toDebugString()}')); + unsupportedInlineCssProperties.add(property); + _hasError = true; + return null; + } } enum KatexSpanFontWeight { @@ -732,6 +782,32 @@ enum KatexSpanTextAlign { right, } +class KatexSpanColor { + const KatexSpanColor(this.r, this.g, this.b, this.a); + + final int r; + final int g; + final int b; + final int a; + + @override + bool operator ==(Object other) { + return other is KatexSpanColor && + other.r == r && + other.g == g && + other.b == b && + other.a == a; + } + + @override + int get hashCode => Object.hash('KatexSpanColor', r, g, b, a); + + @override + String toString() { + return '${objectRuntimeType(this, 'KatexSpanColor')}($r, $g, $b, $a)'; + } +} + @immutable class KatexSpanStyles { // TODO(#1674) does height actually appear on generic spans? @@ -755,6 +831,8 @@ class KatexSpanStyles { final KatexSpanFontStyle? fontStyle; final KatexSpanTextAlign? textAlign; + final KatexSpanColor? color; + const KatexSpanStyles({ this.heightEm, this.topEm, @@ -765,6 +843,7 @@ class KatexSpanStyles { this.fontWeight, this.fontStyle, this.textAlign, + this.color, }); @override @@ -779,6 +858,7 @@ class KatexSpanStyles { fontWeight, fontStyle, textAlign, + color, ); @override @@ -792,7 +872,8 @@ class KatexSpanStyles { other.fontSizeEm == fontSizeEm && other.fontWeight == fontWeight && other.fontStyle == fontStyle && - other.textAlign == textAlign; + other.textAlign == textAlign && + other.color == color; } @override @@ -807,6 +888,7 @@ class KatexSpanStyles { if (fontWeight != null) args.add('fontWeight: $fontWeight'); if (fontStyle != null) args.add('fontStyle: $fontStyle'); if (textAlign != null) args.add('textAlign: $textAlign'); + if (color != null) args.add('color: $color'); return '${objectRuntimeType(this, 'KatexSpanStyles')}(${args.join(', ')})'; } @@ -821,6 +903,7 @@ class KatexSpanStyles { bool fontWeight = true, bool fontStyle = true, bool textAlign = true, + bool color = true, }) { return KatexSpanStyles( heightEm: heightEm ? this.heightEm : null, @@ -832,10 +915,201 @@ class KatexSpanStyles { fontWeight: fontWeight ? this.fontWeight : null, fontStyle: fontStyle ? this.fontStyle : null, textAlign: textAlign ? this.textAlign : null, + color: color ? this.color : null, ); } } +final _hexColorRegExp = + RegExp(r'^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$'); + +/// Parses the CSS hex color notation. +/// +/// See: https://drafts.csswg.org/css-color/#hex-notation +@visibleForTesting +KatexSpanColor? parseCssHexColor(String hexStr) { + final match = _hexColorRegExp.firstMatch(hexStr); + if (match == null) return null; + + String hexValue = match.group(1)!; + switch (hexValue.length) { + case 3: + hexValue = '${hexValue[0]}${hexValue[0]}' + '${hexValue[1]}${hexValue[1]}' + '${hexValue[2]}${hexValue[2]}' + 'ff'; + case 4: + hexValue = '${hexValue[0]}${hexValue[0]}' + '${hexValue[1]}${hexValue[1]}' + '${hexValue[2]}${hexValue[2]}' + '${hexValue[3]}${hexValue[3]}'; + case 6: + hexValue += 'ff'; + } + + try { + final [r, g, b, a] = hex.decode(hexValue); + return KatexSpanColor(r, g, b, a); + } catch (_) { + return null; // TODO(log) + } +} + +// CSS named colors: https://drafts.csswg.org/css-color/#named-colors +// Map adapted from the following source file: +// https://github.com/w3c/csswg-drafts/blob/1942d0918/css-color-4/Overview.bs#L1562-L1859 +const _cssNamedColorsMap = { + 'transparent': KatexSpanColor(0, 0, 0, 0), // https://drafts.csswg.org/css-color/#transparent-color + 'aliceblue': KatexSpanColor(240, 248, 255, 255), + 'antiquewhite': KatexSpanColor(250, 235, 215, 255), + 'aqua': KatexSpanColor(0, 255, 255, 255), + 'aquamarine': KatexSpanColor(127, 255, 212, 255), + 'azure': KatexSpanColor(240, 255, 255, 255), + 'beige': KatexSpanColor(245, 245, 220, 255), + 'bisque': KatexSpanColor(255, 228, 196, 255), + 'black': KatexSpanColor(0, 0, 0, 255), + 'blanchedalmond': KatexSpanColor(255, 235, 205, 255), + 'blue': KatexSpanColor(0, 0, 255, 255), + 'blueviolet': KatexSpanColor(138, 43, 226, 255), + 'brown': KatexSpanColor(165, 42, 42, 255), + 'burlywood': KatexSpanColor(222, 184, 135, 255), + 'cadetblue': KatexSpanColor(95, 158, 160, 255), + 'chartreuse': KatexSpanColor(127, 255, 0, 255), + 'chocolate': KatexSpanColor(210, 105, 30, 255), + 'coral': KatexSpanColor(255, 127, 80, 255), + 'cornflowerblue': KatexSpanColor(100, 149, 237, 255), + 'cornsilk': KatexSpanColor(255, 248, 220, 255), + 'crimson': KatexSpanColor(220, 20, 60, 255), + 'cyan': KatexSpanColor(0, 255, 255, 255), + 'darkblue': KatexSpanColor(0, 0, 139, 255), + 'darkcyan': KatexSpanColor(0, 139, 139, 255), + 'darkgoldenrod': KatexSpanColor(184, 134, 11, 255), + 'darkgray': KatexSpanColor(169, 169, 169, 255), + 'darkgreen': KatexSpanColor(0, 100, 0, 255), + 'darkgrey': KatexSpanColor(169, 169, 169, 255), + 'darkkhaki': KatexSpanColor(189, 183, 107, 255), + 'darkmagenta': KatexSpanColor(139, 0, 139, 255), + 'darkolivegreen': KatexSpanColor(85, 107, 47, 255), + 'darkorange': KatexSpanColor(255, 140, 0, 255), + 'darkorchid': KatexSpanColor(153, 50, 204, 255), + 'darkred': KatexSpanColor(139, 0, 0, 255), + 'darksalmon': KatexSpanColor(233, 150, 122, 255), + 'darkseagreen': KatexSpanColor(143, 188, 143, 255), + 'darkslateblue': KatexSpanColor(72, 61, 139, 255), + 'darkslategray': KatexSpanColor(47, 79, 79, 255), + 'darkslategrey': KatexSpanColor(47, 79, 79, 255), + 'darkturquoise': KatexSpanColor(0, 206, 209, 255), + 'darkviolet': KatexSpanColor(148, 0, 211, 255), + 'deeppink': KatexSpanColor(255, 20, 147, 255), + 'deepskyblue': KatexSpanColor(0, 191, 255, 255), + 'dimgray': KatexSpanColor(105, 105, 105, 255), + 'dimgrey': KatexSpanColor(105, 105, 105, 255), + 'dodgerblue': KatexSpanColor(30, 144, 255, 255), + 'firebrick': KatexSpanColor(178, 34, 34, 255), + 'floralwhite': KatexSpanColor(255, 250, 240, 255), + 'forestgreen': KatexSpanColor(34, 139, 34, 255), + 'fuchsia': KatexSpanColor(255, 0, 255, 255), + 'gainsboro': KatexSpanColor(220, 220, 220, 255), + 'ghostwhite': KatexSpanColor(248, 248, 255, 255), + 'gold': KatexSpanColor(255, 215, 0, 255), + 'goldenrod': KatexSpanColor(218, 165, 32, 255), + 'gray': KatexSpanColor(128, 128, 128, 255), + 'green': KatexSpanColor(0, 128, 0, 255), + 'greenyellow': KatexSpanColor(173, 255, 47, 255), + 'grey': KatexSpanColor(128, 128, 128, 255), + 'honeydew': KatexSpanColor(240, 255, 240, 255), + 'hotpink': KatexSpanColor(255, 105, 180, 255), + 'indianred': KatexSpanColor(205, 92, 92, 255), + 'indigo': KatexSpanColor(75, 0, 130, 255), + 'ivory': KatexSpanColor(255, 255, 240, 255), + 'khaki': KatexSpanColor(240, 230, 140, 255), + 'lavender': KatexSpanColor(230, 230, 250, 255), + 'lavenderblush': KatexSpanColor(255, 240, 245, 255), + 'lawngreen': KatexSpanColor(124, 252, 0, 255), + 'lemonchiffon': KatexSpanColor(255, 250, 205, 255), + 'lightblue': KatexSpanColor(173, 216, 230, 255), + 'lightcoral': KatexSpanColor(240, 128, 128, 255), + 'lightcyan': KatexSpanColor(224, 255, 255, 255), + 'lightgoldenrodyellow': KatexSpanColor(250, 250, 210, 255), + 'lightgray': KatexSpanColor(211, 211, 211, 255), + 'lightgreen': KatexSpanColor(144, 238, 144, 255), + 'lightgrey': KatexSpanColor(211, 211, 211, 255), + 'lightpink': KatexSpanColor(255, 182, 193, 255), + 'lightsalmon': KatexSpanColor(255, 160, 122, 255), + 'lightseagreen': KatexSpanColor(32, 178, 170, 255), + 'lightskyblue': KatexSpanColor(135, 206, 250, 255), + 'lightslategray': KatexSpanColor(119, 136, 153, 255), + 'lightslategrey': KatexSpanColor(119, 136, 153, 255), + 'lightsteelblue': KatexSpanColor(176, 196, 222, 255), + 'lightyellow': KatexSpanColor(255, 255, 224, 255), + 'lime': KatexSpanColor(0, 255, 0, 255), + 'limegreen': KatexSpanColor(50, 205, 50, 255), + 'linen': KatexSpanColor(250, 240, 230, 255), + 'magenta': KatexSpanColor(255, 0, 255, 255), + 'maroon': KatexSpanColor(128, 0, 0, 255), + 'mediumaquamarine': KatexSpanColor(102, 205, 170, 255), + 'mediumblue': KatexSpanColor(0, 0, 205, 255), + 'mediumorchid': KatexSpanColor(186, 85, 211, 255), + 'mediumpurple': KatexSpanColor(147, 112, 219, 255), + 'mediumseagreen': KatexSpanColor(60, 179, 113, 255), + 'mediumslateblue': KatexSpanColor(123, 104, 238, 255), + 'mediumspringgreen': KatexSpanColor(0, 250, 154, 255), + 'mediumturquoise': KatexSpanColor(72, 209, 204, 255), + 'mediumvioletred': KatexSpanColor(199, 21, 133, 255), + 'midnightblue': KatexSpanColor(25, 25, 112, 255), + 'mintcream': KatexSpanColor(245, 255, 250, 255), + 'mistyrose': KatexSpanColor(255, 228, 225, 255), + 'moccasin': KatexSpanColor(255, 228, 181, 255), + 'navajowhite': KatexSpanColor(255, 222, 173, 255), + 'navy': KatexSpanColor(0, 0, 128, 255), + 'oldlace': KatexSpanColor(253, 245, 230, 255), + 'olive': KatexSpanColor(128, 128, 0, 255), + 'olivedrab': KatexSpanColor(107, 142, 35, 255), + 'orange': KatexSpanColor(255, 165, 0, 255), + 'orangered': KatexSpanColor(255, 69, 0, 255), + 'orchid': KatexSpanColor(218, 112, 214, 255), + 'palegoldenrod': KatexSpanColor(238, 232, 170, 255), + 'palegreen': KatexSpanColor(152, 251, 152, 255), + 'paleturquoise': KatexSpanColor(175, 238, 238, 255), + 'palevioletred': KatexSpanColor(219, 112, 147, 255), + 'papayawhip': KatexSpanColor(255, 239, 213, 255), + 'peachpuff': KatexSpanColor(255, 218, 185, 255), + 'peru': KatexSpanColor(205, 133, 63, 255), + 'pink': KatexSpanColor(255, 192, 203, 255), + 'plum': KatexSpanColor(221, 160, 221, 255), + 'powderblue': KatexSpanColor(176, 224, 230, 255), + 'purple': KatexSpanColor(128, 0, 128, 255), + 'rebeccapurple': KatexSpanColor(102, 51, 153, 255), + 'red': KatexSpanColor(255, 0, 0, 255), + 'rosybrown': KatexSpanColor(188, 143, 143, 255), + 'royalblue': KatexSpanColor(65, 105, 225, 255), + 'saddlebrown': KatexSpanColor(139, 69, 19, 255), + 'salmon': KatexSpanColor(250, 128, 114, 255), + 'sandybrown': KatexSpanColor(244, 164, 96, 255), + 'seagreen': KatexSpanColor(46, 139, 87, 255), + 'seashell': KatexSpanColor(255, 245, 238, 255), + 'sienna': KatexSpanColor(160, 82, 45, 255), + 'silver': KatexSpanColor(192, 192, 192, 255), + 'skyblue': KatexSpanColor(135, 206, 235, 255), + 'slateblue': KatexSpanColor(106, 90, 205, 255), + 'slategray': KatexSpanColor(112, 128, 144, 255), + 'slategrey': KatexSpanColor(112, 128, 144, 255), + 'snow': KatexSpanColor(255, 250, 250, 255), + 'springgreen': KatexSpanColor(0, 255, 127, 255), + 'steelblue': KatexSpanColor(70, 130, 180, 255), + 'tan': KatexSpanColor(210, 180, 140, 255), + 'teal': KatexSpanColor(0, 128, 128, 255), + 'thistle': KatexSpanColor(216, 191, 216, 255), + 'tomato': KatexSpanColor(255, 99, 71, 255), + 'turquoise': KatexSpanColor(64, 224, 208, 255), + 'violet': KatexSpanColor(238, 130, 238, 255), + 'wheat': KatexSpanColor(245, 222, 179, 255), + 'white': KatexSpanColor(255, 255, 255, 255), + 'whitesmoke': KatexSpanColor(245, 245, 245, 255), + 'yellow': KatexSpanColor(255, 255, 0, 255), + 'yellowgreen': KatexSpanColor(154, 205, 50, 255), +}; + class _KatexHtmlParseError extends Error { final String? message; diff --git a/lib/widgets/katex.dart b/lib/widgets/katex.dart index 9d439ffdd3..2cae2ce5fe 100644 --- a/lib/widgets/katex.dart +++ b/lib/widgets/katex.dart @@ -117,12 +117,18 @@ class _KatexSpan extends StatelessWidget { KatexSpanFontStyle.italic => FontStyle.italic, null => null, }; + final color = switch (styles.color) { + KatexSpanColor katexColor => + Color.fromARGB(katexColor.a, katexColor.r, katexColor.g, katexColor.b), + null => null, + }; TextStyle? textStyle; if (fontFamily != null || fontSize != null || fontWeight != null || - fontStyle != null) { + fontStyle != null || + color != null) { // TODO(upstream) remove this workaround when upstream fixes the broken // rendering of KaTeX_Math font with italic font style on Android: // https://github.com/flutter/flutter/issues/167474 @@ -136,6 +142,7 @@ class _KatexSpan extends StatelessWidget { fontSize: fontSize, fontWeight: fontWeight, fontStyle: fontStyle, + color: color, ); } final textAlign = switch (styles.textAlign) { diff --git a/test/model/katex_test.dart b/test/model/katex_test.dart index 8cf2ea155a..7c28d013d3 100644 --- a/test/model/katex_test.dart +++ b/test/model/katex_test.dart @@ -566,6 +566,83 @@ class KatexExample extends ContentExample { ]), ]), ]); + + static final color = KatexExample.block( + r'\color: 3-digit hex color', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2232197 + r'\color{#f00} 0', + '

' + '' + '0\\color{#f00} 0' + '

', [ + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), + KatexSpanNode( + styles: KatexSpanStyles(color: KatexSpanColor(255, 0, 0, 255)), + text: '0'), + ]), + ]); + + static final textColor = KatexExample.block( + r'\textcolor: CSS named color', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2232198 + r'\textcolor{red} 1', + '

' + '' + '1\\textcolor{red} 1' + '

', [ + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), + KatexSpanNode( + styles: KatexSpanStyles(color: KatexSpanColor(255, 0, 0, 255)), + text: '1'), + ]), + ]); + + // KaTeX custom color macros, see https://github.com/KaTeX/KaTeX/blob/9fb63136e/src/macros.js#L977-L1033 + static final customColorMacro = KatexExample.block( + r'\red, custom KaTeX color macro: CSS 6-digit hex color', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2232199 + r'\red 2', + '

' + '' + '2\\red 2' + '

', [ + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), + KatexSpanNode( + styles: KatexSpanStyles(color: KatexSpanColor(223, 0, 48, 255)), + text: '2'), + ]), + ]); + + static final phantom = KatexExample.block( + r'\phantom: span with "color: transparent"', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2229515 + r'\phantom{*}', + '

' + '' + '\\phantom{*}' + '

', [ + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 0.4653, verticalAlignEm: null), + KatexSpanNode( + styles: KatexSpanStyles(color: KatexSpanColor(0, 0, 0, 0)), + text: '∗'), + ]), + ]); } void main() async { @@ -582,6 +659,55 @@ void main() async { testParseExample(KatexExample.negativeMargin); testParseExample(KatexExample.katexLogo); testParseExample(KatexExample.vlistNegativeMargin); + testParseExample(KatexExample.color); + testParseExample(KatexExample.textColor); + testParseExample(KatexExample.customColorMacro); + testParseExample(KatexExample.phantom); + + group('parseCssHexColor', () { + const testCases = [ + ('#c0c0c0ff', KatexSpanColor(192, 192, 192, 255)), + ('#f00ba4', KatexSpanColor(240, 11, 164, 255)), + ('#cafe', KatexSpanColor(204, 170, 255, 238)), + + ('#ffffffff', KatexSpanColor(255, 255, 255, 255)), + ('#ffffff', KatexSpanColor(255, 255, 255, 255)), + ('#ffff', KatexSpanColor(255, 255, 255, 255)), + ('#fff', KatexSpanColor(255, 255, 255, 255)), + ('#00ffffff', KatexSpanColor(0, 255, 255, 255)), + ('#00ffff', KatexSpanColor(0, 255, 255, 255)), + ('#0fff', KatexSpanColor(0, 255, 255, 255)), + ('#0ff', KatexSpanColor(0, 255, 255, 255)), + ('#ff00ffff', KatexSpanColor(255, 0, 255, 255)), + ('#ff00ff', KatexSpanColor(255, 0, 255, 255)), + ('#f0ff', KatexSpanColor(255, 0, 255, 255)), + ('#f0f', KatexSpanColor(255, 0, 255, 255)), + ('#ffff00ff', KatexSpanColor(255, 255, 0, 255)), + ('#ffff00', KatexSpanColor(255, 255, 0, 255)), + ('#ff0f', KatexSpanColor(255, 255, 0, 255)), + ('#ff0', KatexSpanColor(255, 255, 0, 255)), + ('#ffffff00', KatexSpanColor(255, 255, 255, 0)), + ('#fff0', KatexSpanColor(255, 255, 255, 0)), + + ('#FF00FFFF', KatexSpanColor(255, 0, 255, 255)), + ('#FF00FF', KatexSpanColor(255, 0, 255, 255)), + + ('#ff00FFff', KatexSpanColor(255, 0, 255, 255)), + ('#ff00FF', KatexSpanColor(255, 0, 255, 255)), + + ('#F', null), + ('#FF', null), + ('#FFFFF', null), + ('#FFFFFFF', null), + ('FFF', null), + ]; + + for (final testCase in testCases) { + test(testCase.$1, () { + check(parseCssHexColor(testCase.$1)).equals(testCase.$2); + }); + } + }); test('all KaTeX content examples are tested', () { // Check that every KatexExample defined above has a corresponding diff --git a/test/widgets/katex_test.dart b/test/widgets/katex_test.dart index 7e44f338e0..11af76c9eb 100644 --- a/test/widgets/katex_test.dart +++ b/test/widgets/katex_test.dart @@ -1,4 +1,5 @@ import 'package:checks/checks.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_checks/flutter_checks.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -99,6 +100,39 @@ void main() { }, skip: testCase.skip); } }); + + group('characters are rendered in specific color', () { + final testCases = <(KatexExample, List<(String, Color)>)>[ + (KatexExample.color, [ + ('0', Color.fromARGB(255, 255, 0, 0)) + ]), + (KatexExample.textColor, [ + ('1', Color.fromARGB(255, 255, 0, 0)) + ]), + (KatexExample.customColorMacro, [ + ('2', Color.fromARGB(255, 223, 0, 48)) + ]), + (KatexExample.phantom, [ + ('∗', Color.fromARGB(0, 0, 0, 0)) + ]) + ]; + + for (final testCase in testCases) { + testWidgets(testCase.$1.description, (tester) async { + await prepareContent(tester, plainContent(testCase.$1.html)); + + for (final characterData in testCase.$2) { + final character = characterData.$1; + final expectedColor = characterData.$2; + + final renderParagraph = + tester.renderObject(find.text(character)); + final color = renderParagraph.text.style?.color; + check(color).equals(expectedColor); + } + }); + } + }); } Future _loadKatexFonts() async {