Skip to content

Commit 4fdd366

Browse files
content: Support colored text in KaTeX content
Fixes: #1679
1 parent c87de41 commit 4fdd366

File tree

4 files changed

+444
-3
lines changed

4 files changed

+444
-3
lines changed

lib/model/katex.dart

Lines changed: 275 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:collection/collection.dart';
2+
import 'package:convert/convert.dart';
23
import 'package:csslib/parser.dart' as css_parser;
34
import 'package:csslib/visitor.dart' as css_visitor;
45
import 'package:flutter/foundation.dart';
@@ -629,6 +630,7 @@ class _KatexParser {
629630
topEm: _takeStyleEm(inlineStyles, 'top'),
630631
marginLeftEm: _takeStyleEm(inlineStyles, 'margin-left'),
631632
marginRightEm: _takeStyleEm(inlineStyles, 'margin-right'),
633+
color: _takeStyleColor(inlineStyles, 'color'),
632634
// TODO handle more CSS properties
633635
);
634636
if (inlineStyles != null && inlineStyles.isNotEmpty) {
@@ -715,6 +717,54 @@ class _KatexParser {
715717
_hasError = true;
716718
return null;
717719
}
720+
721+
/// Remove the given property from the given style map,
722+
/// and parse as a color value.
723+
///
724+
/// If the property is present but is not a valid CSS Hex color,
725+
/// or is not one of the CSS named color, record an error
726+
/// and return null.
727+
///
728+
/// If the property is absent, return null with no error.
729+
///
730+
/// If the map is null, treat it as empty.
731+
///
732+
/// To produce the map this method expects, see [_parseInlineStyles].
733+
KatexSpanColor? _takeStyleColor(Map<String, css_visitor.Expression>? styles, String property) {
734+
final expression = styles?.remove(property);
735+
if (expression == null) return null;
736+
737+
// `package:csslib` parser emits a HexColorTerm for the `color`
738+
// attribute. It automatically resolves the named CSS colors to
739+
// their hex values. The `HexColorTerm.value` is the hex
740+
// encoded in an integer in the same sequence as the input hex
741+
// string. But it also allows some non-conformant CSS hex color
742+
// notations, like #f, #ff, #fffff, #fffffff.
743+
// See:
744+
// https://drafts.csswg.org/css-color/#hex-notation.
745+
// https://github.com/dart-lang/tools/blob/2a2a2d611/pkgs/csslib/lib/parser.dart#L2714-L2743
746+
//
747+
// So, we try to parse the value of `color` attribute ourselves
748+
// only allowing conformant CSS hex color notations, mapping
749+
// named CSS colors to their corresponding values, generating a
750+
// typed result (KatexSpanColor(r, g, b, a)) to be used later
751+
// while rendering.
752+
final valueStr = expression.span?.text;
753+
if (valueStr != null) {
754+
if (valueStr.startsWith('#')) {
755+
final color = parseCssHexColor(valueStr);
756+
if (color != null) return color;
757+
} else {
758+
final color = _cssNamedColorsMap[valueStr];
759+
if (color != null) return color;
760+
}
761+
}
762+
assert(debugLog('KaTeX: Unsupported value for CSS property $property,'
763+
' expected a color: ${expression.toDebugString()}'));
764+
unsupportedInlineCssProperties.add(property);
765+
_hasError = true;
766+
return null;
767+
}
718768
}
719769

720770
enum KatexSpanFontWeight {
@@ -732,6 +782,32 @@ enum KatexSpanTextAlign {
732782
right,
733783
}
734784

785+
class KatexSpanColor {
786+
const KatexSpanColor(this.r, this.g, this.b, this.a);
787+
788+
final int r;
789+
final int g;
790+
final int b;
791+
final int a;
792+
793+
@override
794+
bool operator ==(Object other) {
795+
return other is KatexSpanColor &&
796+
other.r == r &&
797+
other.g == g &&
798+
other.b == b &&
799+
other.a == a;
800+
}
801+
802+
@override
803+
int get hashCode => Object.hash('KatexSpanColor', r, g, b, a);
804+
805+
@override
806+
String toString() {
807+
return '${objectRuntimeType(this, 'KatexSpanColor')}($r, $g, $b, $a)';
808+
}
809+
}
810+
735811
@immutable
736812
class KatexSpanStyles {
737813
// TODO(#1674) does height actually appear on generic spans?
@@ -755,6 +831,8 @@ class KatexSpanStyles {
755831
final KatexSpanFontStyle? fontStyle;
756832
final KatexSpanTextAlign? textAlign;
757833

834+
final KatexSpanColor? color;
835+
758836
const KatexSpanStyles({
759837
this.heightEm,
760838
this.topEm,
@@ -765,6 +843,7 @@ class KatexSpanStyles {
765843
this.fontWeight,
766844
this.fontStyle,
767845
this.textAlign,
846+
this.color,
768847
});
769848

770849
@override
@@ -779,6 +858,7 @@ class KatexSpanStyles {
779858
fontWeight,
780859
fontStyle,
781860
textAlign,
861+
color,
782862
);
783863

784864
@override
@@ -792,7 +872,8 @@ class KatexSpanStyles {
792872
other.fontSizeEm == fontSizeEm &&
793873
other.fontWeight == fontWeight &&
794874
other.fontStyle == fontStyle &&
795-
other.textAlign == textAlign;
875+
other.textAlign == textAlign &&
876+
other.color == color;
796877
}
797878

798879
@override
@@ -807,6 +888,7 @@ class KatexSpanStyles {
807888
if (fontWeight != null) args.add('fontWeight: $fontWeight');
808889
if (fontStyle != null) args.add('fontStyle: $fontStyle');
809890
if (textAlign != null) args.add('textAlign: $textAlign');
891+
if (color != null) args.add('color: $color');
810892
return '${objectRuntimeType(this, 'KatexSpanStyles')}(${args.join(', ')})';
811893
}
812894

@@ -821,6 +903,7 @@ class KatexSpanStyles {
821903
bool fontWeight = true,
822904
bool fontStyle = true,
823905
bool textAlign = true,
906+
bool color = true,
824907
}) {
825908
return KatexSpanStyles(
826909
heightEm: heightEm ? this.heightEm : null,
@@ -832,10 +915,201 @@ class KatexSpanStyles {
832915
fontWeight: fontWeight ? this.fontWeight : null,
833916
fontStyle: fontStyle ? this.fontStyle : null,
834917
textAlign: textAlign ? this.textAlign : null,
918+
color: color ? this.color : null,
835919
);
836920
}
837921
}
838922

923+
final _hexColorRegExp =
924+
RegExp(r'^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$');
925+
926+
/// Parses the CSS hex color notation.
927+
///
928+
/// See: https://drafts.csswg.org/css-color/#hex-notation
929+
@visibleForTesting
930+
KatexSpanColor? parseCssHexColor(String hexStr) {
931+
final match = _hexColorRegExp.firstMatch(hexStr);
932+
if (match == null) return null;
933+
934+
String hexValue = match.group(1)!;
935+
switch (hexValue.length) {
936+
case 3:
937+
hexValue = '${hexValue[0]}${hexValue[0]}'
938+
'${hexValue[1]}${hexValue[1]}'
939+
'${hexValue[2]}${hexValue[2]}'
940+
'ff';
941+
case 4:
942+
hexValue = '${hexValue[0]}${hexValue[0]}'
943+
'${hexValue[1]}${hexValue[1]}'
944+
'${hexValue[2]}${hexValue[2]}'
945+
'${hexValue[3]}${hexValue[3]}';
946+
case 6:
947+
hexValue += 'ff';
948+
}
949+
950+
try {
951+
final [r, g, b, a] = hex.decode(hexValue);
952+
return KatexSpanColor(r, g, b, a);
953+
} catch (_) {
954+
return null; // TODO(log)
955+
}
956+
}
957+
958+
// CSS named colors: https://drafts.csswg.org/css-color/#named-colors
959+
// Map adapted from the following source file:
960+
// https://github.com/w3c/csswg-drafts/blob/1942d0918/css-color-4/Overview.bs#L1562-L1859
961+
const _cssNamedColorsMap = {
962+
'transparent': KatexSpanColor(0, 0, 0, 0), // https://drafts.csswg.org/css-color/#transparent-color
963+
'aliceblue': KatexSpanColor(240, 248, 255, 255),
964+
'antiquewhite': KatexSpanColor(250, 235, 215, 255),
965+
'aqua': KatexSpanColor(0, 255, 255, 255),
966+
'aquamarine': KatexSpanColor(127, 255, 212, 255),
967+
'azure': KatexSpanColor(240, 255, 255, 255),
968+
'beige': KatexSpanColor(245, 245, 220, 255),
969+
'bisque': KatexSpanColor(255, 228, 196, 255),
970+
'black': KatexSpanColor(0, 0, 0, 255),
971+
'blanchedalmond': KatexSpanColor(255, 235, 205, 255),
972+
'blue': KatexSpanColor(0, 0, 255, 255),
973+
'blueviolet': KatexSpanColor(138, 43, 226, 255),
974+
'brown': KatexSpanColor(165, 42, 42, 255),
975+
'burlywood': KatexSpanColor(222, 184, 135, 255),
976+
'cadetblue': KatexSpanColor(95, 158, 160, 255),
977+
'chartreuse': KatexSpanColor(127, 255, 0, 255),
978+
'chocolate': KatexSpanColor(210, 105, 30, 255),
979+
'coral': KatexSpanColor(255, 127, 80, 255),
980+
'cornflowerblue': KatexSpanColor(100, 149, 237, 255),
981+
'cornsilk': KatexSpanColor(255, 248, 220, 255),
982+
'crimson': KatexSpanColor(220, 20, 60, 255),
983+
'cyan': KatexSpanColor(0, 255, 255, 255),
984+
'darkblue': KatexSpanColor(0, 0, 139, 255),
985+
'darkcyan': KatexSpanColor(0, 139, 139, 255),
986+
'darkgoldenrod': KatexSpanColor(184, 134, 11, 255),
987+
'darkgray': KatexSpanColor(169, 169, 169, 255),
988+
'darkgreen': KatexSpanColor(0, 100, 0, 255),
989+
'darkgrey': KatexSpanColor(169, 169, 169, 255),
990+
'darkkhaki': KatexSpanColor(189, 183, 107, 255),
991+
'darkmagenta': KatexSpanColor(139, 0, 139, 255),
992+
'darkolivegreen': KatexSpanColor(85, 107, 47, 255),
993+
'darkorange': KatexSpanColor(255, 140, 0, 255),
994+
'darkorchid': KatexSpanColor(153, 50, 204, 255),
995+
'darkred': KatexSpanColor(139, 0, 0, 255),
996+
'darksalmon': KatexSpanColor(233, 150, 122, 255),
997+
'darkseagreen': KatexSpanColor(143, 188, 143, 255),
998+
'darkslateblue': KatexSpanColor(72, 61, 139, 255),
999+
'darkslategray': KatexSpanColor(47, 79, 79, 255),
1000+
'darkslategrey': KatexSpanColor(47, 79, 79, 255),
1001+
'darkturquoise': KatexSpanColor(0, 206, 209, 255),
1002+
'darkviolet': KatexSpanColor(148, 0, 211, 255),
1003+
'deeppink': KatexSpanColor(255, 20, 147, 255),
1004+
'deepskyblue': KatexSpanColor(0, 191, 255, 255),
1005+
'dimgray': KatexSpanColor(105, 105, 105, 255),
1006+
'dimgrey': KatexSpanColor(105, 105, 105, 255),
1007+
'dodgerblue': KatexSpanColor(30, 144, 255, 255),
1008+
'firebrick': KatexSpanColor(178, 34, 34, 255),
1009+
'floralwhite': KatexSpanColor(255, 250, 240, 255),
1010+
'forestgreen': KatexSpanColor(34, 139, 34, 255),
1011+
'fuchsia': KatexSpanColor(255, 0, 255, 255),
1012+
'gainsboro': KatexSpanColor(220, 220, 220, 255),
1013+
'ghostwhite': KatexSpanColor(248, 248, 255, 255),
1014+
'gold': KatexSpanColor(255, 215, 0, 255),
1015+
'goldenrod': KatexSpanColor(218, 165, 32, 255),
1016+
'gray': KatexSpanColor(128, 128, 128, 255),
1017+
'green': KatexSpanColor(0, 128, 0, 255),
1018+
'greenyellow': KatexSpanColor(173, 255, 47, 255),
1019+
'grey': KatexSpanColor(128, 128, 128, 255),
1020+
'honeydew': KatexSpanColor(240, 255, 240, 255),
1021+
'hotpink': KatexSpanColor(255, 105, 180, 255),
1022+
'indianred': KatexSpanColor(205, 92, 92, 255),
1023+
'indigo': KatexSpanColor(75, 0, 130, 255),
1024+
'ivory': KatexSpanColor(255, 255, 240, 255),
1025+
'khaki': KatexSpanColor(240, 230, 140, 255),
1026+
'lavender': KatexSpanColor(230, 230, 250, 255),
1027+
'lavenderblush': KatexSpanColor(255, 240, 245, 255),
1028+
'lawngreen': KatexSpanColor(124, 252, 0, 255),
1029+
'lemonchiffon': KatexSpanColor(255, 250, 205, 255),
1030+
'lightblue': KatexSpanColor(173, 216, 230, 255),
1031+
'lightcoral': KatexSpanColor(240, 128, 128, 255),
1032+
'lightcyan': KatexSpanColor(224, 255, 255, 255),
1033+
'lightgoldenrodyellow': KatexSpanColor(250, 250, 210, 255),
1034+
'lightgray': KatexSpanColor(211, 211, 211, 255),
1035+
'lightgreen': KatexSpanColor(144, 238, 144, 255),
1036+
'lightgrey': KatexSpanColor(211, 211, 211, 255),
1037+
'lightpink': KatexSpanColor(255, 182, 193, 255),
1038+
'lightsalmon': KatexSpanColor(255, 160, 122, 255),
1039+
'lightseagreen': KatexSpanColor(32, 178, 170, 255),
1040+
'lightskyblue': KatexSpanColor(135, 206, 250, 255),
1041+
'lightslategray': KatexSpanColor(119, 136, 153, 255),
1042+
'lightslategrey': KatexSpanColor(119, 136, 153, 255),
1043+
'lightsteelblue': KatexSpanColor(176, 196, 222, 255),
1044+
'lightyellow': KatexSpanColor(255, 255, 224, 255),
1045+
'lime': KatexSpanColor(0, 255, 0, 255),
1046+
'limegreen': KatexSpanColor(50, 205, 50, 255),
1047+
'linen': KatexSpanColor(250, 240, 230, 255),
1048+
'magenta': KatexSpanColor(255, 0, 255, 255),
1049+
'maroon': KatexSpanColor(128, 0, 0, 255),
1050+
'mediumaquamarine': KatexSpanColor(102, 205, 170, 255),
1051+
'mediumblue': KatexSpanColor(0, 0, 205, 255),
1052+
'mediumorchid': KatexSpanColor(186, 85, 211, 255),
1053+
'mediumpurple': KatexSpanColor(147, 112, 219, 255),
1054+
'mediumseagreen': KatexSpanColor(60, 179, 113, 255),
1055+
'mediumslateblue': KatexSpanColor(123, 104, 238, 255),
1056+
'mediumspringgreen': KatexSpanColor(0, 250, 154, 255),
1057+
'mediumturquoise': KatexSpanColor(72, 209, 204, 255),
1058+
'mediumvioletred': KatexSpanColor(199, 21, 133, 255),
1059+
'midnightblue': KatexSpanColor(25, 25, 112, 255),
1060+
'mintcream': KatexSpanColor(245, 255, 250, 255),
1061+
'mistyrose': KatexSpanColor(255, 228, 225, 255),
1062+
'moccasin': KatexSpanColor(255, 228, 181, 255),
1063+
'navajowhite': KatexSpanColor(255, 222, 173, 255),
1064+
'navy': KatexSpanColor(0, 0, 128, 255),
1065+
'oldlace': KatexSpanColor(253, 245, 230, 255),
1066+
'olive': KatexSpanColor(128, 128, 0, 255),
1067+
'olivedrab': KatexSpanColor(107, 142, 35, 255),
1068+
'orange': KatexSpanColor(255, 165, 0, 255),
1069+
'orangered': KatexSpanColor(255, 69, 0, 255),
1070+
'orchid': KatexSpanColor(218, 112, 214, 255),
1071+
'palegoldenrod': KatexSpanColor(238, 232, 170, 255),
1072+
'palegreen': KatexSpanColor(152, 251, 152, 255),
1073+
'paleturquoise': KatexSpanColor(175, 238, 238, 255),
1074+
'palevioletred': KatexSpanColor(219, 112, 147, 255),
1075+
'papayawhip': KatexSpanColor(255, 239, 213, 255),
1076+
'peachpuff': KatexSpanColor(255, 218, 185, 255),
1077+
'peru': KatexSpanColor(205, 133, 63, 255),
1078+
'pink': KatexSpanColor(255, 192, 203, 255),
1079+
'plum': KatexSpanColor(221, 160, 221, 255),
1080+
'powderblue': KatexSpanColor(176, 224, 230, 255),
1081+
'purple': KatexSpanColor(128, 0, 128, 255),
1082+
'rebeccapurple': KatexSpanColor(102, 51, 153, 255),
1083+
'red': KatexSpanColor(255, 0, 0, 255),
1084+
'rosybrown': KatexSpanColor(188, 143, 143, 255),
1085+
'royalblue': KatexSpanColor(65, 105, 225, 255),
1086+
'saddlebrown': KatexSpanColor(139, 69, 19, 255),
1087+
'salmon': KatexSpanColor(250, 128, 114, 255),
1088+
'sandybrown': KatexSpanColor(244, 164, 96, 255),
1089+
'seagreen': KatexSpanColor(46, 139, 87, 255),
1090+
'seashell': KatexSpanColor(255, 245, 238, 255),
1091+
'sienna': KatexSpanColor(160, 82, 45, 255),
1092+
'silver': KatexSpanColor(192, 192, 192, 255),
1093+
'skyblue': KatexSpanColor(135, 206, 235, 255),
1094+
'slateblue': KatexSpanColor(106, 90, 205, 255),
1095+
'slategray': KatexSpanColor(112, 128, 144, 255),
1096+
'slategrey': KatexSpanColor(112, 128, 144, 255),
1097+
'snow': KatexSpanColor(255, 250, 250, 255),
1098+
'springgreen': KatexSpanColor(0, 255, 127, 255),
1099+
'steelblue': KatexSpanColor(70, 130, 180, 255),
1100+
'tan': KatexSpanColor(210, 180, 140, 255),
1101+
'teal': KatexSpanColor(0, 128, 128, 255),
1102+
'thistle': KatexSpanColor(216, 191, 216, 255),
1103+
'tomato': KatexSpanColor(255, 99, 71, 255),
1104+
'turquoise': KatexSpanColor(64, 224, 208, 255),
1105+
'violet': KatexSpanColor(238, 130, 238, 255),
1106+
'wheat': KatexSpanColor(245, 222, 179, 255),
1107+
'white': KatexSpanColor(255, 255, 255, 255),
1108+
'whitesmoke': KatexSpanColor(245, 245, 245, 255),
1109+
'yellow': KatexSpanColor(255, 255, 0, 255),
1110+
'yellowgreen': KatexSpanColor(154, 205, 50, 255),
1111+
};
1112+
8391113
class _KatexHtmlParseError extends Error {
8401114
final String? message;
8411115

lib/widgets/katex.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,18 @@ class _KatexSpan extends StatelessWidget {
117117
KatexSpanFontStyle.italic => FontStyle.italic,
118118
null => null,
119119
};
120+
final color = switch (styles.color) {
121+
KatexSpanColor katexColor =>
122+
Color.fromARGB(katexColor.a, katexColor.r, katexColor.g, katexColor.b),
123+
null => null,
124+
};
120125

121126
TextStyle? textStyle;
122127
if (fontFamily != null ||
123128
fontSize != null ||
124129
fontWeight != null ||
125-
fontStyle != null) {
130+
fontStyle != null ||
131+
color != null) {
126132
// TODO(upstream) remove this workaround when upstream fixes the broken
127133
// rendering of KaTeX_Math font with italic font style on Android:
128134
// https://github.com/flutter/flutter/issues/167474
@@ -136,6 +142,7 @@ class _KatexSpan extends StatelessWidget {
136142
fontSize: fontSize,
137143
fontWeight: fontWeight,
138144
fontStyle: fontStyle,
145+
color: color,
139146
);
140147
}
141148
final textAlign = switch (styles.textAlign) {

0 commit comments

Comments
 (0)