Skip to content

Commit 6a377e0

Browse files
content: Support colored text in KaTeX content
1 parent 0ada484 commit 6a377e0

File tree

5 files changed

+392
-3
lines changed

5 files changed

+392
-3
lines changed

lib/model/katex.dart

Lines changed: 243 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import 'package:convert/convert.dart';
12
import 'package:csslib/parser.dart' as css_parser;
23
import 'package:csslib/visitor.dart' as css_visitor;
34
import 'package:flutter/foundation.dart';
4-
import 'package:flutter/widgets.dart';
55
import 'package:html/dom.dart' as dom;
66

77
import '../log.dart';
@@ -463,6 +463,7 @@ class _KatexParser {
463463
final stylesheet = css_parser.parse('*{$styleStr}');
464464
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
465465
double? heightEm;
466+
KatexSpanColor? color;
466467

467468
for (final declaration in rule.declarationGroup.declarations) {
468469
if (declaration case css_visitor.Declaration(
@@ -474,6 +475,18 @@ class _KatexParser {
474475
case 'height':
475476
heightEm = _getEm(expression);
476477
if (heightEm != null) continue;
478+
479+
case 'color':
480+
final valueStr = _getRawValue(expression);
481+
if (valueStr != null) {
482+
if (valueStr.startsWith('#')) {
483+
color = parseCssHexColor(valueStr);
484+
if (color != null) continue;
485+
}
486+
487+
color = _cssNamedColorsMap[valueStr];
488+
if (color != null) continue;
489+
}
477490
}
478491

479492
// TODO handle more CSS properties
@@ -488,6 +501,7 @@ class _KatexParser {
488501

489502
return KatexSpanStyles(
490503
heightEm: heightEm,
504+
color: color,
491505
);
492506
} else {
493507
throw _KatexHtmlParseError();
@@ -504,6 +518,10 @@ class _KatexParser {
504518
}
505519
return null;
506520
}
521+
522+
String? _getRawValue(css_visitor.Expression expression) {
523+
return expression.span?.text;
524+
}
507525
}
508526

509527
enum KatexSpanFontWeight {
@@ -521,6 +539,32 @@ enum KatexSpanTextAlign {
521539
right,
522540
}
523541

542+
class KatexSpanColor {
543+
const KatexSpanColor(this.r, this.g, this.b, this.a);
544+
545+
final int r;
546+
final int g;
547+
final int b;
548+
final int a;
549+
550+
@override
551+
bool operator ==(Object other) {
552+
return other is KatexSpanColor &&
553+
other.r == r &&
554+
other.g == g &&
555+
other.b == b &&
556+
other.a == a;
557+
}
558+
559+
@override
560+
int get hashCode => Object.hash('KatexSpanColor', r, g, b, a);
561+
562+
@override
563+
String toString() {
564+
return '${objectRuntimeType(this, 'KatexSpanColor')}($r, $g, $b, $a)';
565+
}
566+
}
567+
524568
@immutable
525569
class KatexSpanStyles {
526570
final double? heightEm;
@@ -531,13 +575,16 @@ class KatexSpanStyles {
531575
final KatexSpanFontStyle? fontStyle;
532576
final KatexSpanTextAlign? textAlign;
533577

578+
final KatexSpanColor? color;
579+
534580
const KatexSpanStyles({
535581
this.heightEm,
536582
this.fontFamily,
537583
this.fontSizeEm,
538584
this.fontWeight,
539585
this.fontStyle,
540586
this.textAlign,
587+
this.color,
541588
});
542589

543590
@override
@@ -549,6 +596,7 @@ class KatexSpanStyles {
549596
fontWeight,
550597
fontStyle,
551598
textAlign,
599+
color,
552600
);
553601

554602
@override
@@ -559,7 +607,8 @@ class KatexSpanStyles {
559607
other.fontSizeEm == fontSizeEm &&
560608
other.fontWeight == fontWeight &&
561609
other.fontStyle == fontStyle &&
562-
other.textAlign == textAlign;
610+
other.textAlign == textAlign &&
611+
other.color == color;
563612
}
564613

565614
@override
@@ -571,6 +620,7 @@ class KatexSpanStyles {
571620
if (fontWeight != null) args.add('fontWeight: $fontWeight');
572621
if (fontStyle != null) args.add('fontStyle: $fontStyle');
573622
if (textAlign != null) args.add('textAlign: $textAlign');
623+
if (color != null) args.add('color: $color');
574624
return '${objectRuntimeType(this, 'KatexSpanStyles')}(${args.join(', ')})';
575625
}
576626

@@ -589,10 +639,201 @@ class KatexSpanStyles {
589639
fontStyle: other.fontStyle ?? fontStyle,
590640
fontWeight: other.fontWeight ?? fontWeight,
591641
textAlign: other.textAlign ?? textAlign,
642+
color: other.color ?? color,
592643
);
593644
}
594645
}
595646

647+
final _hexColorRegExp =
648+
RegExp(r'^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$');
649+
650+
/// Parses the CSS hex color notation.
651+
///
652+
/// See: https://drafts.csswg.org/css-color/#hex-notation
653+
KatexSpanColor? parseCssHexColor(String hexStr) {
654+
final match = _hexColorRegExp.firstMatch(hexStr);
655+
if (match == null) return null;
656+
657+
String hexValue = match.group(1)!;
658+
hexValue = hexValue.toLowerCase();
659+
switch (hexValue.length) {
660+
case 3:
661+
hexValue = '${hexValue[0]}${hexValue[0]}'
662+
'${hexValue[1]}${hexValue[1]}'
663+
'${hexValue[2]}${hexValue[2]}'
664+
'ff';
665+
case 4:
666+
hexValue = '${hexValue[0]}${hexValue[0]}'
667+
'${hexValue[1]}${hexValue[1]}'
668+
'${hexValue[2]}${hexValue[2]}'
669+
'${hexValue[3]}${hexValue[3]}';
670+
case 6:
671+
hexValue += 'ff';
672+
}
673+
674+
try {
675+
final [r, g, b, a] = hex.decode(hexValue);
676+
return KatexSpanColor(r, g, b, a);
677+
} catch (_) {
678+
return null; // TODO(log)
679+
}
680+
}
681+
682+
// CSS named colors: https://drafts.csswg.org/css-color/#named-colors
683+
// Map adapted from the following source file:
684+
// https://github.com/w3c/csswg-drafts/blob/1942d0918/css-color-4/Overview.bs#L1562-L1859
685+
const _cssNamedColorsMap = {
686+
'transparent': KatexSpanColor(0, 0, 0, 0), // https://drafts.csswg.org/css-color/#transparent-color
687+
'aliceblue': KatexSpanColor(240, 248, 255, 255),
688+
'antiquewhite': KatexSpanColor(250, 235, 215, 255),
689+
'aqua': KatexSpanColor(0, 255, 255, 255),
690+
'aquamarine': KatexSpanColor(127, 255, 212, 255),
691+
'azure': KatexSpanColor(240, 255, 255, 255),
692+
'beige': KatexSpanColor(245, 245, 220, 255),
693+
'bisque': KatexSpanColor(255, 228, 196, 255),
694+
'black': KatexSpanColor(0, 0, 0, 255),
695+
'blanchedalmond': KatexSpanColor(255, 235, 205, 255),
696+
'blue': KatexSpanColor(0, 0, 255, 255),
697+
'blueviolet': KatexSpanColor(138, 43, 226, 255),
698+
'brown': KatexSpanColor(165, 42, 42, 255),
699+
'burlywood': KatexSpanColor(222, 184, 135, 255),
700+
'cadetblue': KatexSpanColor(95, 158, 160, 255),
701+
'chartreuse': KatexSpanColor(127, 255, 0, 255),
702+
'chocolate': KatexSpanColor(210, 105, 30, 255),
703+
'coral': KatexSpanColor(255, 127, 80, 255),
704+
'cornflowerblue': KatexSpanColor(100, 149, 237, 255),
705+
'cornsilk': KatexSpanColor(255, 248, 220, 255),
706+
'crimson': KatexSpanColor(220, 20, 60, 255),
707+
'cyan': KatexSpanColor(0, 255, 255, 255),
708+
'darkblue': KatexSpanColor(0, 0, 139, 255),
709+
'darkcyan': KatexSpanColor(0, 139, 139, 255),
710+
'darkgoldenrod': KatexSpanColor(184, 134, 11, 255),
711+
'darkgray': KatexSpanColor(169, 169, 169, 255),
712+
'darkgreen': KatexSpanColor(0, 100, 0, 255),
713+
'darkgrey': KatexSpanColor(169, 169, 169, 255),
714+
'darkkhaki': KatexSpanColor(189, 183, 107, 255),
715+
'darkmagenta': KatexSpanColor(139, 0, 139, 255),
716+
'darkolivegreen': KatexSpanColor(85, 107, 47, 255),
717+
'darkorange': KatexSpanColor(255, 140, 0, 255),
718+
'darkorchid': KatexSpanColor(153, 50, 204, 255),
719+
'darkred': KatexSpanColor(139, 0, 0, 255),
720+
'darksalmon': KatexSpanColor(233, 150, 122, 255),
721+
'darkseagreen': KatexSpanColor(143, 188, 143, 255),
722+
'darkslateblue': KatexSpanColor(72, 61, 139, 255),
723+
'darkslategray': KatexSpanColor(47, 79, 79, 255),
724+
'darkslategrey': KatexSpanColor(47, 79, 79, 255),
725+
'darkturquoise': KatexSpanColor(0, 206, 209, 255),
726+
'darkviolet': KatexSpanColor(148, 0, 211, 255),
727+
'deeppink': KatexSpanColor(255, 20, 147, 255),
728+
'deepskyblue': KatexSpanColor(0, 191, 255, 255),
729+
'dimgray': KatexSpanColor(105, 105, 105, 255),
730+
'dimgrey': KatexSpanColor(105, 105, 105, 255),
731+
'dodgerblue': KatexSpanColor(30, 144, 255, 255),
732+
'firebrick': KatexSpanColor(178, 34, 34, 255),
733+
'floralwhite': KatexSpanColor(255, 250, 240, 255),
734+
'forestgreen': KatexSpanColor(34, 139, 34, 255),
735+
'fuchsia': KatexSpanColor(255, 0, 255, 255),
736+
'gainsboro': KatexSpanColor(220, 220, 220, 255),
737+
'ghostwhite': KatexSpanColor(248, 248, 255, 255),
738+
'gold': KatexSpanColor(255, 215, 0, 255),
739+
'goldenrod': KatexSpanColor(218, 165, 32, 255),
740+
'gray': KatexSpanColor(128, 128, 128, 255),
741+
'green': KatexSpanColor(0, 128, 0, 255),
742+
'greenyellow': KatexSpanColor(173, 255, 47, 255),
743+
'grey': KatexSpanColor(128, 128, 128, 255),
744+
'honeydew': KatexSpanColor(240, 255, 240, 255),
745+
'hotpink': KatexSpanColor(255, 105, 180, 255),
746+
'indianred': KatexSpanColor(205, 92, 92, 255),
747+
'indigo': KatexSpanColor(75, 0, 130, 255),
748+
'ivory': KatexSpanColor(255, 255, 240, 255),
749+
'khaki': KatexSpanColor(240, 230, 140, 255),
750+
'lavender': KatexSpanColor(230, 230, 250, 255),
751+
'lavenderblush': KatexSpanColor(255, 240, 245, 255),
752+
'lawngreen': KatexSpanColor(124, 252, 0, 255),
753+
'lemonchiffon': KatexSpanColor(255, 250, 205, 255),
754+
'lightblue': KatexSpanColor(173, 216, 230, 255),
755+
'lightcoral': KatexSpanColor(240, 128, 128, 255),
756+
'lightcyan': KatexSpanColor(224, 255, 255, 255),
757+
'lightgoldenrodyellow': KatexSpanColor(250, 250, 210, 255),
758+
'lightgray': KatexSpanColor(211, 211, 211, 255),
759+
'lightgreen': KatexSpanColor(144, 238, 144, 255),
760+
'lightgrey': KatexSpanColor(211, 211, 211, 255),
761+
'lightpink': KatexSpanColor(255, 182, 193, 255),
762+
'lightsalmon': KatexSpanColor(255, 160, 122, 255),
763+
'lightseagreen': KatexSpanColor(32, 178, 170, 255),
764+
'lightskyblue': KatexSpanColor(135, 206, 250, 255),
765+
'lightslategray': KatexSpanColor(119, 136, 153, 255),
766+
'lightslategrey': KatexSpanColor(119, 136, 153, 255),
767+
'lightsteelblue': KatexSpanColor(176, 196, 222, 255),
768+
'lightyellow': KatexSpanColor(255, 255, 224, 255),
769+
'lime': KatexSpanColor(0, 255, 0, 255),
770+
'limegreen': KatexSpanColor(50, 205, 50, 255),
771+
'linen': KatexSpanColor(250, 240, 230, 255),
772+
'magenta': KatexSpanColor(255, 0, 255, 255),
773+
'maroon': KatexSpanColor(128, 0, 0, 255),
774+
'mediumaquamarine': KatexSpanColor(102, 205, 170, 255),
775+
'mediumblue': KatexSpanColor(0, 0, 205, 255),
776+
'mediumorchid': KatexSpanColor(186, 85, 211, 255),
777+
'mediumpurple': KatexSpanColor(147, 112, 219, 255),
778+
'mediumseagreen': KatexSpanColor(60, 179, 113, 255),
779+
'mediumslateblue': KatexSpanColor(123, 104, 238, 255),
780+
'mediumspringgreen': KatexSpanColor(0, 250, 154, 255),
781+
'mediumturquoise': KatexSpanColor(72, 209, 204, 255),
782+
'mediumvioletred': KatexSpanColor(199, 21, 133, 255),
783+
'midnightblue': KatexSpanColor(25, 25, 112, 255),
784+
'mintcream': KatexSpanColor(245, 255, 250, 255),
785+
'mistyrose': KatexSpanColor(255, 228, 225, 255),
786+
'moccasin': KatexSpanColor(255, 228, 181, 255),
787+
'navajowhite': KatexSpanColor(255, 222, 173, 255),
788+
'navy': KatexSpanColor(0, 0, 128, 255),
789+
'oldlace': KatexSpanColor(253, 245, 230, 255),
790+
'olive': KatexSpanColor(128, 128, 0, 255),
791+
'olivedrab': KatexSpanColor(107, 142, 35, 255),
792+
'orange': KatexSpanColor(255, 165, 0, 255),
793+
'orangered': KatexSpanColor(255, 69, 0, 255),
794+
'orchid': KatexSpanColor(218, 112, 214, 255),
795+
'palegoldenrod': KatexSpanColor(238, 232, 170, 255),
796+
'palegreen': KatexSpanColor(152, 251, 152, 255),
797+
'paleturquoise': KatexSpanColor(175, 238, 238, 255),
798+
'palevioletred': KatexSpanColor(219, 112, 147, 255),
799+
'papayawhip': KatexSpanColor(255, 239, 213, 255),
800+
'peachpuff': KatexSpanColor(255, 218, 185, 255),
801+
'peru': KatexSpanColor(205, 133, 63, 255),
802+
'pink': KatexSpanColor(255, 192, 203, 255),
803+
'plum': KatexSpanColor(221, 160, 221, 255),
804+
'powderblue': KatexSpanColor(176, 224, 230, 255),
805+
'purple': KatexSpanColor(128, 0, 128, 255),
806+
'rebeccapurple': KatexSpanColor(102, 51, 153, 255),
807+
'red': KatexSpanColor(255, 0, 0, 255),
808+
'rosybrown': KatexSpanColor(188, 143, 143, 255),
809+
'royalblue': KatexSpanColor(65, 105, 225, 255),
810+
'saddlebrown': KatexSpanColor(139, 69, 19, 255),
811+
'salmon': KatexSpanColor(250, 128, 114, 255),
812+
'sandybrown': KatexSpanColor(244, 164, 96, 255),
813+
'seagreen': KatexSpanColor(46, 139, 87, 255),
814+
'seashell': KatexSpanColor(255, 245, 238, 255),
815+
'sienna': KatexSpanColor(160, 82, 45, 255),
816+
'silver': KatexSpanColor(192, 192, 192, 255),
817+
'skyblue': KatexSpanColor(135, 206, 235, 255),
818+
'slateblue': KatexSpanColor(106, 90, 205, 255),
819+
'slategray': KatexSpanColor(112, 128, 144, 255),
820+
'slategrey': KatexSpanColor(112, 128, 144, 255),
821+
'snow': KatexSpanColor(255, 250, 250, 255),
822+
'springgreen': KatexSpanColor(0, 255, 127, 255),
823+
'steelblue': KatexSpanColor(70, 130, 180, 255),
824+
'tan': KatexSpanColor(210, 180, 140, 255),
825+
'teal': KatexSpanColor(0, 128, 128, 255),
826+
'thistle': KatexSpanColor(216, 191, 216, 255),
827+
'tomato': KatexSpanColor(255, 99, 71, 255),
828+
'turquoise': KatexSpanColor(64, 224, 208, 255),
829+
'violet': KatexSpanColor(238, 130, 238, 255),
830+
'wheat': KatexSpanColor(245, 222, 179, 255),
831+
'white': KatexSpanColor(255, 255, 255, 255),
832+
'whitesmoke': KatexSpanColor(245, 245, 245, 255),
833+
'yellow': KatexSpanColor(255, 255, 0, 255),
834+
'yellowgreen': KatexSpanColor(154, 205, 50, 255),
835+
};
836+
596837
class _KatexHtmlParseError extends Error {
597838
final String? message;
598839

lib/widgets/content.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -911,12 +911,14 @@ class _KatexSpan extends StatelessWidget {
911911
KatexSpanFontStyle.italic => FontStyle.italic,
912912
null => null,
913913
};
914+
final color = styles.color;
914915

915916
TextStyle? textStyle;
916917
if (fontFamily != null ||
917918
fontSize != null ||
918919
fontWeight != null ||
919-
fontStyle != null) {
920+
fontStyle != null ||
921+
color != null) {
920922
// TODO(upstream) remove this workaround when upstream fixes the broken
921923
// rendering of KaTeX_Math font with italic font style on Android:
922924
// https://github.com/flutter/flutter/issues/167474
@@ -930,6 +932,9 @@ class _KatexSpan extends StatelessWidget {
930932
fontSize: fontSize,
931933
fontWeight: fontWeight,
932934
fontStyle: fontStyle,
935+
color: color != null
936+
? Color.fromARGB(color.a, color.r, color.g, color.b)
937+
: null,
933938
);
934939
}
935940
final textAlign = switch (styles.textAlign) {

0 commit comments

Comments
 (0)