Skip to content

KaTeX(8/n): Support colored text in KaTeX content #1670

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 265 additions & 2 deletions lib/model/katex.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'dart:ui';

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';
import 'package:flutter/widgets.dart';
import 'package:html/dom.dart' as dom;

import '../log.dart';
Expand Down Expand Up @@ -494,6 +496,7 @@ class _KatexParser {
double? verticalAlignEm;
double? marginRightEm;
double? marginLeftEm;
KatexSpanColor? color;

for (final declaration in rule.declarationGroup.declarations) {
if (declaration case css_visitor.Declaration(
Expand Down Expand Up @@ -523,6 +526,38 @@ class _KatexParser {
if (marginLeftEm < 0) throw _KatexHtmlParseError();
continue;
}

case 'color':
// `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
//
// Also the generated integer value will be in 0xRRGGBBAA
// sequence (CSS notation), whereas `dart:ui` Color
// requires 0xAARRGGBB.
//
// 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 = _getRawValue(expression);
if (valueStr != null) {
if (valueStr.startsWith('#')) {
color = parseCssHexColor(valueStr);
if (color != null) continue;
}

color = _cssNamedColorsMap[valueStr];
if (color != null) continue;
}
}

// TODO handle more CSS properties
Expand All @@ -540,6 +575,7 @@ class _KatexParser {
verticalAlignEm: verticalAlignEm,
marginRightEm: marginRightEm,
marginLeftEm: marginLeftEm,
color: color,
);
} else {
throw _KatexHtmlParseError();
Expand All @@ -556,6 +592,10 @@ class _KatexParser {
}
return null;
}

String? _getRawValue(css_visitor.Expression expression) {
return expression.span?.text;
}
}

enum KatexSpanFontWeight {
Expand All @@ -573,6 +613,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 {
final double? heightEm;
Expand All @@ -587,6 +653,8 @@ class KatexSpanStyles {
final KatexSpanFontStyle? fontStyle;
final KatexSpanTextAlign? textAlign;

final KatexSpanColor? color;

const KatexSpanStyles({
this.heightEm,
this.verticalAlignEm,
Expand All @@ -597,6 +665,7 @@ class KatexSpanStyles {
this.fontWeight,
this.fontStyle,
this.textAlign,
this.color,
});

@override
Expand All @@ -611,6 +680,7 @@ class KatexSpanStyles {
fontWeight,
fontStyle,
textAlign,
color,
);

@override
Expand All @@ -624,7 +694,8 @@ class KatexSpanStyles {
other.fontSizeEm == fontSizeEm &&
other.fontWeight == fontWeight &&
other.fontStyle == fontStyle &&
other.textAlign == textAlign;
other.textAlign == textAlign &&
other.color == color;
}

@override
Expand All @@ -639,6 +710,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(', ')})';
}

Expand All @@ -660,6 +732,7 @@ class KatexSpanStyles {
fontStyle: other.fontStyle ?? fontStyle,
fontWeight: other.fontWeight ?? fontWeight,
textAlign: other.textAlign ?? textAlign,
color: other.color ?? color,
);
}

Expand Down Expand Up @@ -688,6 +761,196 @@ class KatexSpanStyles {
}
}

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
KatexSpanColor? parseCssHexColor(String hexStr) {
final match = _hexColorRegExp.firstMatch(hexStr);
if (match == null) return null;

String hexValue = match.group(1)!;
hexValue = hexValue.toLowerCase();
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;

Expand Down
7 changes: 6 additions & 1 deletion lib/widgets/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -940,12 +940,14 @@ class _KatexSpan extends StatelessWidget {
KatexSpanFontStyle.italic => FontStyle.italic,
null => null,
};
final color = styles.color;

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
Expand All @@ -959,6 +961,9 @@ class _KatexSpan extends StatelessWidget {
fontSize: fontSize,
fontWeight: fontWeight,
fontStyle: fontStyle,
color: color != null
? Color.fromARGB(color.a, color.r, color.g, color.b)
: null,
);
}
final textAlign = switch (styles.textAlign) {
Expand Down
Loading
Loading