Skip to content

Commit ad2a745

Browse files
content: Support colored text in KaTeX content
Fixes: #1679
1 parent 94037d7 commit ad2a745

File tree

5 files changed

+405
-3
lines changed

5 files changed

+405
-3
lines changed

lib/model/katex.dart

Lines changed: 265 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import 'dart:ui';
2+
3+
import 'package:convert/convert.dart';
14
import 'package:csslib/parser.dart' as css_parser;
25
import 'package:csslib/visitor.dart' as css_visitor;
36
import 'package:flutter/foundation.dart';
4-
import 'package:flutter/widgets.dart';
57
import 'package:html/dom.dart' as dom;
68

79
import '../log.dart';
@@ -494,6 +496,7 @@ class _KatexParser {
494496
double? verticalAlignEm;
495497
double? marginRightEm;
496498
double? marginLeftEm;
499+
KatexSpanColor? color;
497500

498501
for (final declaration in rule.declarationGroup.declarations) {
499502
if (declaration case css_visitor.Declaration(
@@ -523,6 +526,38 @@ class _KatexParser {
523526
if (marginLeftEm < 0) throw _KatexHtmlParseError();
524527
continue;
525528
}
529+
530+
case 'color':
531+
// `package:csslib` parser emits a HexColorTerm for the `color`
532+
// attribute. It automatically resolves the named CSS colors to
533+
// their hex values. The `HexColorTerm.value` is the hex
534+
// encoded in an integer in the same sequence as the input hex
535+
// string. But it also allows some non-conformant CSS hex color
536+
// notations, like #f, #ff, #fffff, #fffffff.
537+
// See:
538+
// https://drafts.csswg.org/css-color/#hex-notation.
539+
// https://github.com/dart-lang/tools/blob/2a2a2d611/pkgs/csslib/lib/parser.dart#L2714-L2743
540+
//
541+
// Also the generated integer value will be in 0xRRGGBBAA
542+
// sequence (CSS notation), whereas `dart:ui` Color
543+
// requires 0xAARRGGBB.
544+
//
545+
// So, we try to parse the value of `color` attribute ourselves
546+
// only allowing conformant CSS hex color notations, mapping
547+
// named CSS colors to their corresponding values, generating a
548+
// typed result (KatexSpanColor(r, g, b, a)) to be used later
549+
// while rendering.
550+
551+
final valueStr = _getRawValue(expression);
552+
if (valueStr != null) {
553+
if (valueStr.startsWith('#')) {
554+
color = parseCssHexColor(valueStr);
555+
if (color != null) continue;
556+
}
557+
558+
color = _cssNamedColorsMap[valueStr];
559+
if (color != null) continue;
560+
}
526561
}
527562

528563
// TODO handle more CSS properties
@@ -540,6 +575,7 @@ class _KatexParser {
540575
verticalAlignEm: verticalAlignEm,
541576
marginRightEm: marginRightEm,
542577
marginLeftEm: marginLeftEm,
578+
color: color,
543579
);
544580
} else {
545581
throw _KatexHtmlParseError();
@@ -556,6 +592,10 @@ class _KatexParser {
556592
}
557593
return null;
558594
}
595+
596+
String? _getRawValue(css_visitor.Expression expression) {
597+
return expression.span?.text;
598+
}
559599
}
560600

561601
enum KatexSpanFontWeight {
@@ -573,6 +613,32 @@ enum KatexSpanTextAlign {
573613
right,
574614
}
575615

616+
class KatexSpanColor {
617+
const KatexSpanColor(this.r, this.g, this.b, this.a);
618+
619+
final int r;
620+
final int g;
621+
final int b;
622+
final int a;
623+
624+
@override
625+
bool operator ==(Object other) {
626+
return other is KatexSpanColor &&
627+
other.r == r &&
628+
other.g == g &&
629+
other.b == b &&
630+
other.a == a;
631+
}
632+
633+
@override
634+
int get hashCode => Object.hash('KatexSpanColor', r, g, b, a);
635+
636+
@override
637+
String toString() {
638+
return '${objectRuntimeType(this, 'KatexSpanColor')}($r, $g, $b, $a)';
639+
}
640+
}
641+
576642
@immutable
577643
class KatexSpanStyles {
578644
final double? heightEm;
@@ -587,6 +653,8 @@ class KatexSpanStyles {
587653
final KatexSpanFontStyle? fontStyle;
588654
final KatexSpanTextAlign? textAlign;
589655

656+
final KatexSpanColor? color;
657+
590658
const KatexSpanStyles({
591659
this.heightEm,
592660
this.verticalAlignEm,
@@ -597,6 +665,7 @@ class KatexSpanStyles {
597665
this.fontWeight,
598666
this.fontStyle,
599667
this.textAlign,
668+
this.color,
600669
});
601670

602671
@override
@@ -611,6 +680,7 @@ class KatexSpanStyles {
611680
fontWeight,
612681
fontStyle,
613682
textAlign,
683+
color,
614684
);
615685

616686
@override
@@ -624,7 +694,8 @@ class KatexSpanStyles {
624694
other.fontSizeEm == fontSizeEm &&
625695
other.fontWeight == fontWeight &&
626696
other.fontStyle == fontStyle &&
627-
other.textAlign == textAlign;
697+
other.textAlign == textAlign &&
698+
other.color == color;
628699
}
629700

630701
@override
@@ -639,6 +710,7 @@ class KatexSpanStyles {
639710
if (fontWeight != null) args.add('fontWeight: $fontWeight');
640711
if (fontStyle != null) args.add('fontStyle: $fontStyle');
641712
if (textAlign != null) args.add('textAlign: $textAlign');
713+
if (color != null) args.add('color: $color');
642714
return '${objectRuntimeType(this, 'KatexSpanStyles')}(${args.join(', ')})';
643715
}
644716

@@ -660,6 +732,7 @@ class KatexSpanStyles {
660732
fontStyle: other.fontStyle ?? fontStyle,
661733
fontWeight: other.fontWeight ?? fontWeight,
662734
textAlign: other.textAlign ?? textAlign,
735+
color: other.color ?? color,
663736
);
664737
}
665738

@@ -688,6 +761,196 @@ class KatexSpanStyles {
688761
}
689762
}
690763

764+
final _hexColorRegExp =
765+
RegExp(r'^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$');
766+
767+
/// Parses the CSS hex color notation.
768+
///
769+
/// See: https://drafts.csswg.org/css-color/#hex-notation
770+
KatexSpanColor? parseCssHexColor(String hexStr) {
771+
final match = _hexColorRegExp.firstMatch(hexStr);
772+
if (match == null) return null;
773+
774+
String hexValue = match.group(1)!;
775+
hexValue = hexValue.toLowerCase();
776+
switch (hexValue.length) {
777+
case 3:
778+
hexValue = '${hexValue[0]}${hexValue[0]}'
779+
'${hexValue[1]}${hexValue[1]}'
780+
'${hexValue[2]}${hexValue[2]}'
781+
'ff';
782+
case 4:
783+
hexValue = '${hexValue[0]}${hexValue[0]}'
784+
'${hexValue[1]}${hexValue[1]}'
785+
'${hexValue[2]}${hexValue[2]}'
786+
'${hexValue[3]}${hexValue[3]}';
787+
case 6:
788+
hexValue += 'ff';
789+
}
790+
791+
try {
792+
final [r, g, b, a] = hex.decode(hexValue);
793+
return KatexSpanColor(r, g, b, a);
794+
} catch (_) {
795+
return null; // TODO(log)
796+
}
797+
}
798+
799+
// CSS named colors: https://drafts.csswg.org/css-color/#named-colors
800+
// Map adapted from the following source file:
801+
// https://github.com/w3c/csswg-drafts/blob/1942d0918/css-color-4/Overview.bs#L1562-L1859
802+
const _cssNamedColorsMap = {
803+
'transparent': KatexSpanColor(0, 0, 0, 0), // https://drafts.csswg.org/css-color/#transparent-color
804+
'aliceblue': KatexSpanColor(240, 248, 255, 255),
805+
'antiquewhite': KatexSpanColor(250, 235, 215, 255),
806+
'aqua': KatexSpanColor(0, 255, 255, 255),
807+
'aquamarine': KatexSpanColor(127, 255, 212, 255),
808+
'azure': KatexSpanColor(240, 255, 255, 255),
809+
'beige': KatexSpanColor(245, 245, 220, 255),
810+
'bisque': KatexSpanColor(255, 228, 196, 255),
811+
'black': KatexSpanColor(0, 0, 0, 255),
812+
'blanchedalmond': KatexSpanColor(255, 235, 205, 255),
813+
'blue': KatexSpanColor(0, 0, 255, 255),
814+
'blueviolet': KatexSpanColor(138, 43, 226, 255),
815+
'brown': KatexSpanColor(165, 42, 42, 255),
816+
'burlywood': KatexSpanColor(222, 184, 135, 255),
817+
'cadetblue': KatexSpanColor(95, 158, 160, 255),
818+
'chartreuse': KatexSpanColor(127, 255, 0, 255),
819+
'chocolate': KatexSpanColor(210, 105, 30, 255),
820+
'coral': KatexSpanColor(255, 127, 80, 255),
821+
'cornflowerblue': KatexSpanColor(100, 149, 237, 255),
822+
'cornsilk': KatexSpanColor(255, 248, 220, 255),
823+
'crimson': KatexSpanColor(220, 20, 60, 255),
824+
'cyan': KatexSpanColor(0, 255, 255, 255),
825+
'darkblue': KatexSpanColor(0, 0, 139, 255),
826+
'darkcyan': KatexSpanColor(0, 139, 139, 255),
827+
'darkgoldenrod': KatexSpanColor(184, 134, 11, 255),
828+
'darkgray': KatexSpanColor(169, 169, 169, 255),
829+
'darkgreen': KatexSpanColor(0, 100, 0, 255),
830+
'darkgrey': KatexSpanColor(169, 169, 169, 255),
831+
'darkkhaki': KatexSpanColor(189, 183, 107, 255),
832+
'darkmagenta': KatexSpanColor(139, 0, 139, 255),
833+
'darkolivegreen': KatexSpanColor(85, 107, 47, 255),
834+
'darkorange': KatexSpanColor(255, 140, 0, 255),
835+
'darkorchid': KatexSpanColor(153, 50, 204, 255),
836+
'darkred': KatexSpanColor(139, 0, 0, 255),
837+
'darksalmon': KatexSpanColor(233, 150, 122, 255),
838+
'darkseagreen': KatexSpanColor(143, 188, 143, 255),
839+
'darkslateblue': KatexSpanColor(72, 61, 139, 255),
840+
'darkslategray': KatexSpanColor(47, 79, 79, 255),
841+
'darkslategrey': KatexSpanColor(47, 79, 79, 255),
842+
'darkturquoise': KatexSpanColor(0, 206, 209, 255),
843+
'darkviolet': KatexSpanColor(148, 0, 211, 255),
844+
'deeppink': KatexSpanColor(255, 20, 147, 255),
845+
'deepskyblue': KatexSpanColor(0, 191, 255, 255),
846+
'dimgray': KatexSpanColor(105, 105, 105, 255),
847+
'dimgrey': KatexSpanColor(105, 105, 105, 255),
848+
'dodgerblue': KatexSpanColor(30, 144, 255, 255),
849+
'firebrick': KatexSpanColor(178, 34, 34, 255),
850+
'floralwhite': KatexSpanColor(255, 250, 240, 255),
851+
'forestgreen': KatexSpanColor(34, 139, 34, 255),
852+
'fuchsia': KatexSpanColor(255, 0, 255, 255),
853+
'gainsboro': KatexSpanColor(220, 220, 220, 255),
854+
'ghostwhite': KatexSpanColor(248, 248, 255, 255),
855+
'gold': KatexSpanColor(255, 215, 0, 255),
856+
'goldenrod': KatexSpanColor(218, 165, 32, 255),
857+
'gray': KatexSpanColor(128, 128, 128, 255),
858+
'green': KatexSpanColor(0, 128, 0, 255),
859+
'greenyellow': KatexSpanColor(173, 255, 47, 255),
860+
'grey': KatexSpanColor(128, 128, 128, 255),
861+
'honeydew': KatexSpanColor(240, 255, 240, 255),
862+
'hotpink': KatexSpanColor(255, 105, 180, 255),
863+
'indianred': KatexSpanColor(205, 92, 92, 255),
864+
'indigo': KatexSpanColor(75, 0, 130, 255),
865+
'ivory': KatexSpanColor(255, 255, 240, 255),
866+
'khaki': KatexSpanColor(240, 230, 140, 255),
867+
'lavender': KatexSpanColor(230, 230, 250, 255),
868+
'lavenderblush': KatexSpanColor(255, 240, 245, 255),
869+
'lawngreen': KatexSpanColor(124, 252, 0, 255),
870+
'lemonchiffon': KatexSpanColor(255, 250, 205, 255),
871+
'lightblue': KatexSpanColor(173, 216, 230, 255),
872+
'lightcoral': KatexSpanColor(240, 128, 128, 255),
873+
'lightcyan': KatexSpanColor(224, 255, 255, 255),
874+
'lightgoldenrodyellow': KatexSpanColor(250, 250, 210, 255),
875+
'lightgray': KatexSpanColor(211, 211, 211, 255),
876+
'lightgreen': KatexSpanColor(144, 238, 144, 255),
877+
'lightgrey': KatexSpanColor(211, 211, 211, 255),
878+
'lightpink': KatexSpanColor(255, 182, 193, 255),
879+
'lightsalmon': KatexSpanColor(255, 160, 122, 255),
880+
'lightseagreen': KatexSpanColor(32, 178, 170, 255),
881+
'lightskyblue': KatexSpanColor(135, 206, 250, 255),
882+
'lightslategray': KatexSpanColor(119, 136, 153, 255),
883+
'lightslategrey': KatexSpanColor(119, 136, 153, 255),
884+
'lightsteelblue': KatexSpanColor(176, 196, 222, 255),
885+
'lightyellow': KatexSpanColor(255, 255, 224, 255),
886+
'lime': KatexSpanColor(0, 255, 0, 255),
887+
'limegreen': KatexSpanColor(50, 205, 50, 255),
888+
'linen': KatexSpanColor(250, 240, 230, 255),
889+
'magenta': KatexSpanColor(255, 0, 255, 255),
890+
'maroon': KatexSpanColor(128, 0, 0, 255),
891+
'mediumaquamarine': KatexSpanColor(102, 205, 170, 255),
892+
'mediumblue': KatexSpanColor(0, 0, 205, 255),
893+
'mediumorchid': KatexSpanColor(186, 85, 211, 255),
894+
'mediumpurple': KatexSpanColor(147, 112, 219, 255),
895+
'mediumseagreen': KatexSpanColor(60, 179, 113, 255),
896+
'mediumslateblue': KatexSpanColor(123, 104, 238, 255),
897+
'mediumspringgreen': KatexSpanColor(0, 250, 154, 255),
898+
'mediumturquoise': KatexSpanColor(72, 209, 204, 255),
899+
'mediumvioletred': KatexSpanColor(199, 21, 133, 255),
900+
'midnightblue': KatexSpanColor(25, 25, 112, 255),
901+
'mintcream': KatexSpanColor(245, 255, 250, 255),
902+
'mistyrose': KatexSpanColor(255, 228, 225, 255),
903+
'moccasin': KatexSpanColor(255, 228, 181, 255),
904+
'navajowhite': KatexSpanColor(255, 222, 173, 255),
905+
'navy': KatexSpanColor(0, 0, 128, 255),
906+
'oldlace': KatexSpanColor(253, 245, 230, 255),
907+
'olive': KatexSpanColor(128, 128, 0, 255),
908+
'olivedrab': KatexSpanColor(107, 142, 35, 255),
909+
'orange': KatexSpanColor(255, 165, 0, 255),
910+
'orangered': KatexSpanColor(255, 69, 0, 255),
911+
'orchid': KatexSpanColor(218, 112, 214, 255),
912+
'palegoldenrod': KatexSpanColor(238, 232, 170, 255),
913+
'palegreen': KatexSpanColor(152, 251, 152, 255),
914+
'paleturquoise': KatexSpanColor(175, 238, 238, 255),
915+
'palevioletred': KatexSpanColor(219, 112, 147, 255),
916+
'papayawhip': KatexSpanColor(255, 239, 213, 255),
917+
'peachpuff': KatexSpanColor(255, 218, 185, 255),
918+
'peru': KatexSpanColor(205, 133, 63, 255),
919+
'pink': KatexSpanColor(255, 192, 203, 255),
920+
'plum': KatexSpanColor(221, 160, 221, 255),
921+
'powderblue': KatexSpanColor(176, 224, 230, 255),
922+
'purple': KatexSpanColor(128, 0, 128, 255),
923+
'rebeccapurple': KatexSpanColor(102, 51, 153, 255),
924+
'red': KatexSpanColor(255, 0, 0, 255),
925+
'rosybrown': KatexSpanColor(188, 143, 143, 255),
926+
'royalblue': KatexSpanColor(65, 105, 225, 255),
927+
'saddlebrown': KatexSpanColor(139, 69, 19, 255),
928+
'salmon': KatexSpanColor(250, 128, 114, 255),
929+
'sandybrown': KatexSpanColor(244, 164, 96, 255),
930+
'seagreen': KatexSpanColor(46, 139, 87, 255),
931+
'seashell': KatexSpanColor(255, 245, 238, 255),
932+
'sienna': KatexSpanColor(160, 82, 45, 255),
933+
'silver': KatexSpanColor(192, 192, 192, 255),
934+
'skyblue': KatexSpanColor(135, 206, 235, 255),
935+
'slateblue': KatexSpanColor(106, 90, 205, 255),
936+
'slategray': KatexSpanColor(112, 128, 144, 255),
937+
'slategrey': KatexSpanColor(112, 128, 144, 255),
938+
'snow': KatexSpanColor(255, 250, 250, 255),
939+
'springgreen': KatexSpanColor(0, 255, 127, 255),
940+
'steelblue': KatexSpanColor(70, 130, 180, 255),
941+
'tan': KatexSpanColor(210, 180, 140, 255),
942+
'teal': KatexSpanColor(0, 128, 128, 255),
943+
'thistle': KatexSpanColor(216, 191, 216, 255),
944+
'tomato': KatexSpanColor(255, 99, 71, 255),
945+
'turquoise': KatexSpanColor(64, 224, 208, 255),
946+
'violet': KatexSpanColor(238, 130, 238, 255),
947+
'wheat': KatexSpanColor(245, 222, 179, 255),
948+
'white': KatexSpanColor(255, 255, 255, 255),
949+
'whitesmoke': KatexSpanColor(245, 245, 245, 255),
950+
'yellow': KatexSpanColor(255, 255, 0, 255),
951+
'yellowgreen': KatexSpanColor(154, 205, 50, 255),
952+
};
953+
691954
class _KatexHtmlParseError extends Error {
692955
final String? message;
693956

lib/widgets/content.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -940,12 +940,14 @@ class _KatexSpan extends StatelessWidget {
940940
KatexSpanFontStyle.italic => FontStyle.italic,
941941
null => null,
942942
};
943+
final color = styles.color;
943944

944945
TextStyle? textStyle;
945946
if (fontFamily != null ||
946947
fontSize != null ||
947948
fontWeight != null ||
948-
fontStyle != null) {
949+
fontStyle != null ||
950+
color != null) {
949951
// TODO(upstream) remove this workaround when upstream fixes the broken
950952
// rendering of KaTeX_Math font with italic font style on Android:
951953
// https://github.com/flutter/flutter/issues/167474
@@ -959,6 +961,9 @@ class _KatexSpan extends StatelessWidget {
959961
fontSize: fontSize,
960962
fontWeight: fontWeight,
961963
fontStyle: fontStyle,
964+
color: color != null
965+
? Color.fromARGB(color.a, color.r, color.g, color.b)
966+
: null,
962967
);
963968
}
964969
final textAlign = switch (styles.textAlign) {

0 commit comments

Comments
 (0)