Skip to content

Instantly share code, notes, and snippets.

@seven332
Created March 22, 2020 02:59
Show Gist options
  • Save seven332/9dc76255d959c8c5194cbd92068b0f60 to your computer and use it in GitHub Desktop.
Save seven332/9dc76255d959c8c5194cbd92068b0f60 to your computer and use it in GitHub Desktop.
flutter html to InlineSpan
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:html/dom.dart';
import 'package:html/parser.dart' as parser;
import 'html_color.dart' as htmlColor;
// TODO Add tag handler
// TODO Add GestureRecognizer for a tag
InlineSpan parse(String html, TextStyle style) {
final document = parser.parse(html);
final span = _parseRecursive(document.body, style, true);
return span ?? TextSpan();
}
InlineSpan _parseRecursive(Node node, TextStyle style, bool styleChanged) {
if (node is Text) {
return _parseText(node, style, styleChanged);
} else if (node is Element) {
return _parseElement(node, style, styleChanged);
} else {
return _parseOtherNode(node, style, styleChanged);
}
}
// TODO the method always remove whitespace at leading,
// but it should check the tail of the previous span
String _fixWhitespaceInText(String text) {
final sb = new StringBuffer();
int pre = ' '.codeUnitAt(0);
for (int c in text.codeUnits) {
if (c == ' '.codeUnitAt(0) || c == '\n'.codeUnitAt(0)) {
if (pre != ' '.codeUnitAt(0) && pre != '\n'.codeUnitAt(0)) {
sb.writeCharCode(' '.codeUnitAt(0));
pre = c;
}
} else {
sb.writeCharCode(c);
pre = c;
}
}
return sb.toString();
}
InlineSpan _parseText(Text text, TextStyle style, bool styleChanged) {
var t = text.data;
if (t == null || t.isEmpty) return null;
t = _fixWhitespaceInText(t);
return TextSpan(text: t, style: styleChanged ? style : null);
}
TextDecoration _combine(TextDecoration nullable, TextDecoration nonnull) {
if (nullable == null) return nonnull;
else return TextDecoration.combine([nullable, nonnull]);
}
InlineSpan _parseElement(Element element, TextStyle style, bool styleChanged) {
final tag = element.localName.toLowerCase();
GestureRecognizer recognizer;
switch (tag) {
case "body":
// Ignore
break;
case "br":
return TextSpan(text: "\n", style: styleChanged ? style : null);
case "strong":
case "b":
style = style.copyWith(fontWeight: FontWeight.bold);
styleChanged = true;
break;
case "em":
case "cite":
case "dfn":
case "i":
style = style.copyWith(fontStyle: FontStyle.italic);
styleChanged = true;
break;
case "u":
case "ins":
style = style.copyWith(decoration: _combine(style.decoration, TextDecoration.underline));
styleChanged = true;
break;
case "del":
case "s":
case "strike":
style = style.copyWith(decoration: _combine(style.decoration, TextDecoration.lineThrough));
styleChanged = true;
break;
case "font":
Color color = htmlColor.tryParse(element.attributes['color']);
if (color != null) {
style = style.copyWith(color: color);
styleChanged = true;
}
break;
default:
print("Unhandled tag: $tag");
break;
}
return _parseParent(element, style, styleChanged, recognizer);
}
InlineSpan _parseOtherNode(Node node, TextStyle style, bool styleChanged) {
return _parseParent(node, style, styleChanged, null);
}
InlineSpan _parseParent(
Node node,
TextStyle style,
bool styleChanged,
GestureRecognizer recognizer
) {
final children = List<InlineSpan>();
node.nodes.forEach((item) {
// The change of style is applied below
final span = _parseRecursive(item, style, false);
if (span != null) children.add(span);
});
// Avoid TextSpan with no child
if (children.length == 0) return null;
// Avoid TextSpan with only one child
if (children.length == 1) {
final span = children.single;
if (span is TextSpan) {
// Keep origin style/recognizer, or use parent style/recognizer
if ((span.style != null || !styleChanged) &&
(span.recognizer != null || recognizer == null))
return span;
else return TextSpan(
text: span.text,
style: span.style != null ? span.style : style,
recognizer: span.recognizer != null ? span.recognizer : recognizer,
);
}
// TODO what if it's not TextSpan
}
return TextSpan(
children: children,
style: styleChanged ? style : null,
recognizer: recognizer,
);
}
import 'package:flutter/painting.dart';
const _kColorMap = <String, Color>{
'aliceblue': Color(0xFFF0F8FF),
'antiquewhite': Color(0xFFFAEBD7),
'aqua': Color(0xFF00FFFF),
'aquamarine': Color(0xFF7FFFD4),
'azure': Color(0xFFF0FFFF),
'beige': Color(0xFFF5F5DC),
'bisque': Color(0xFFFFE4C4),
'black': Color(0xFF000000),
'blanchedalmond': Color(0xFFFFEBCD),
'blue': Color(0xFF0000FF),
'blueviolet': Color(0xFF8A2BE2),
'brown': Color(0xFFA52A2A),
'burlywood': Color(0xFFDEB887),
'cadetblue': Color(0xFF5F9EA0),
'chartreuse': Color(0xFF7FFF00),
'chocolate': Color(0xFFD2691E),
'coral': Color(0xFFFF7F50),
'cornflowerblue': Color(0xFF6495ED),
'cornsilk': Color(0xFFFFF8DC),
'crimson': Color(0xFFDC143C),
'cyan': Color(0xFF00FFFF),
'darkblue': Color(0xFF00008B),
'darkcyan': Color(0xFF008B8B),
'darkgoldenrod': Color(0xFFB8860B),
'darkgray': Color(0xFFA9A9A9),
'darkgrey': Color(0xFFA9A9A9),
'darkgreen': Color(0xFF006400),
'darkkhaki': Color(0xFFBDB76B),
'darkmagenta': Color(0xFF8B008B),
'darkolivegreen': Color(0xFF556B2F),
'darkorange': Color(0xFFFF8C00),
'darkorchid': Color(0xFF9932CC),
'darkred': Color(0xFF8B0000),
'darksalmon': Color(0xFFE9967A),
'darkseagreen': Color(0xFF8FBC8F),
'darkslateblue': Color(0xFF483D8B),
'darkslategray': Color(0xFF2F4F4F),
'darkslategrey': Color(0xFF2F4F4F),
'darkturquoise': Color(0xFF00CED1),
'darkviolet': Color(0xFF9400D3),
'deeppink': Color(0xFFFF1493),
'deepskyblue': Color(0xFF00BFFF),
'dimgray': Color(0xFF696969),
'dimgrey': Color(0xFF696969),
'dodgerblue': Color(0xFF1E90FF),
'firebrick': Color(0xFFB22222),
'floralwhite': Color(0xFFFFFAF0),
'forestgreen': Color(0xFF228B22),
'fuchsia': Color(0xFFFF00FF),
'gainsboro': Color(0xFFDCDCDC),
'ghostwhite': Color(0xFFF8F8FF),
'gold': Color(0xFFFFD700),
'goldenrod': Color(0xFFDAA520),
'gray': Color(0xFF808080),
'grey': Color(0xFF808080),
'green': Color(0xFF008000),
'greenyellow': Color(0xFFADFF2F),
'honeydew': Color(0xFFF0FFF0),
'hotpink': Color(0xFFFF69B4),
'indianred': Color(0xFFCD5C5C),
'indigo': Color(0xFF4B0082),
'ivory': Color(0xFFFFFFF0),
'khaki': Color(0xFFF0E68C),
'lavender': Color(0xFFE6E6FA),
'lavenderblush': Color(0xFFFFF0F5),
'lawngreen': Color(0xFF7CFC00),
'lemonchiffon': Color(0xFFFFFACD),
'lightblue': Color(0xFFADD8E6),
'lightcoral': Color(0xFFF08080),
'lightcyan': Color(0xFFE0FFFF),
'lightgoldenrodyellow': Color(0xFFFAFAD2),
'lightgray': Color(0xFFD3D3D3),
'lightgrey': Color(0xFFD3D3D3),
'lightgreen': Color(0xFF90EE90),
'lightpink': Color(0xFFFFB6C1),
'lightsalmon': Color(0xFFFFA07A),
'lightseagreen': Color(0xFF20B2AA),
'lightskyblue': Color(0xFF87CEFA),
'lightslategray': Color(0xFF778899),
'lightslategrey': Color(0xFF778899),
'lightsteelblue': Color(0xFFB0C4DE),
'lightyellow': Color(0xFFFFFFE0),
'lime': Color(0xFF00FF00),
'limegreen': Color(0xFF32CD32),
'linen': Color(0xFFFAF0E6),
'magenta': Color(0xFFFF00FF),
'maroon': Color(0xFF800000),
'mediumaquamarine': Color(0xFF66CDAA),
'mediumblue': Color(0xFF0000CD),
'mediumorchid': Color(0xFFBA55D3),
'mediumpurple': Color(0xFF9370DB),
'mediumseagreen': Color(0xFF3CB371),
'mediumslateblue': Color(0xFF7B68EE),
'mediumspringgreen': Color(0xFF00FA9A),
'mediumturquoise': Color(0xFF48D1CC),
'mediumvioletred': Color(0xFFC71585),
'midnightblue': Color(0xFF191970),
'mintcream': Color(0xFFF5FFFA),
'mistyrose': Color(0xFFFFE4E1),
'moccasin': Color(0xFFFFE4B5),
'navajowhite': Color(0xFFFFDEAD),
'navy': Color(0xFF000080),
'oldlace': Color(0xFFFDF5E6),
'olive': Color(0xFF808000),
'olivedrab': Color(0xFF6B8E23),
'orange': Color(0xFFFFA500),
'orangered': Color(0xFFFF4500),
'orchid': Color(0xFFDA70D6),
'palegoldenrod': Color(0xFFEEE8AA),
'palegreen': Color(0xFF98FB98),
'paleturquoise': Color(0xFFAFEEEE),
'palevioletred': Color(0xFFDB7093),
'papayawhip': Color(0xFFFFEFD5),
'peachpuff': Color(0xFFFFDAB9),
'peru': Color(0xFFCD853F),
'pink': Color(0xFFFFC0CB),
'plum': Color(0xFFDDA0DD),
'powderblue': Color(0xFFB0E0E6),
'purple': Color(0xFF800080),
'rebeccapurple': Color(0xFF663399),
'red': Color(0xFFFF0000),
'rosybrown': Color(0xFFBC8F8F),
'royalblue': Color(0xFF4169E1),
'saddlebrown': Color(0xFF8B4513),
'salmon': Color(0xFFFA8072),
'sandybrown': Color(0xFFF4A460),
'seagreen': Color(0xFF2E8B57),
'seashell': Color(0xFFFFF5EE),
'sienna': Color(0xFFA0522D),
'silver': Color(0xFFC0C0C0),
'skyblue': Color(0xFF87CEEB),
'slateblue': Color(0xFF6A5ACD),
'slategray': Color(0xFF708090),
'slategrey': Color(0xFF708090),
'snow': Color(0xFFFFFAFA),
'springgreen': Color(0xFF00FF7F),
'steelblue': Color(0xFF4682B4),
'tan': Color(0xFFD2B48C),
'teal': Color(0xFF008080),
'thistle': Color(0xFFD8BFD8),
'tomato': Color(0xFFFF6347),
'turquoise': Color(0xFF40E0D0),
'violet': Color(0xFFEE82EE),
'wheat': Color(0xFFF5DEB3),
'white': Color(0xFFFFFFFF),
'whitesmoke': Color(0xFFF5F5F5),
'yellow': Color(0xFFFFFF00),
'yellowgreen': Color(0xFF9ACD32),
};
Color parse(String text) {
text = text?.trim();
if (text == null || text.isEmpty) throw FormatException('Empty html color: $text');
if (text.codeUnitAt(0) == '#'.codeUnitAt(0)) {
if (text.length >= 7) {
// #ff0000
return Color(0xFF000000 | int.parse(text.substring(1, 7), radix: 16));
} else if (text.length >= 4) {
// #f00
final color = int.parse(text.substring(1, 4), radix: 16);
final r = color & 0xF00;
final g = color & 0xF0;
final b = color & 0xF;
return Color(0xFF000000 | r << 12 | r << 8 | g << 8 | g << 4 | b << 4 | b);
}
} else if (text.startsWith('rgb(') && text.endsWith(')')) {
// rgb(255, 0, 0)
// rgb(100%, 0%, 0%)
String str = text.substring(4, text.length - 1);
List<String> colors = str.split(',');
if (colors.length >= 3) {
final r = _parseRGB(colors[0]);
final g = _parseRGB(colors[1]);
final b = _parseRGB(colors[2]);
return Color.fromARGB(0xFF, r, g, b);
}
} else if (text.startsWith('rgba(') && text.endsWith(')')) {
// rgba(255, 0, 0, 0.6)
// rgba(100%, 0%, 0%, 0.6)
String str = text.substring(5, text.length - 1);
List<String> colors = str.split(',');
if (colors.length >= 4) {
int r = _parseRGB(colors[0]);
int g = _parseRGB(colors[1]);
int b = _parseRGB(colors[2]);
int a = _parseA(colors[3]);
return Color.fromARGB(a, r, g, b);
}
} else if (text.startsWith('hsl(') && text.endsWith(')')) {
// hsl(120, 100%, 50%)
String str = text.substring(4, text.length - 1);
List<String> colors = str.split(',');
if (colors.length >= 3) {
final h = _parseH(colors[0]);
final s = _parseSL(colors[1]);
final l = _parseSL(colors[2]);
return _hslaToColor(h, s, l, 0xFF);
}
} else if (text.startsWith('hsla(') && text.endsWith(')')) {
// hsla(120, 100%, 25%, 0.3)
String str = text.substring(5, text.length - 1);
List<String> colors = str.split(',');
if (colors.length >= 4) {
final h = _parseH(colors[0]);
final s = _parseSL(colors[1]);
final l = _parseSL(colors[2]);
final a = _parseA(colors[3]);
return _hslaToColor(h, s, l, a);
}
}
final color = _kColorMap[text.toLowerCase()];
if (color == null) throw FormatException('Invalid html color: $text');
return color;
}
Color tryParse(String text) {
try {
return parse(text);
} on FormatException {
return null;
}
}
int _parseRGB(String text) {
text = text.trim();
int i;
if (text.endsWith('%')) {
i = (255 * int.parse(text.substring(0, text.length - 1)) / 100).round();
} else {
i = int.parse(text);
}
return i.clamp(0, 255);
}
int _parseA(String text) {
return (255 * double.parse(text.trim())).round().clamp(0, 255);
}
int _parseH(String text) {
return int.parse(text.trim()) % 360;
}
double _parseSL(String text) {
text = text.trim();
if (text.endsWith('%')) {
final f = int.parse(text.substring(0, text.length - 1)) / 100;
return f.clamp(0, 1);
}
throw FormatException('Invalid s or l in HSL: $text');
}
Color _hslaToColor(int h, double s, double l, int a) {
final c = (1 - (2 * l - 1).abs()) * s;
final m = l - 0.5 * c;
final x = c * (1 - ((h / 60 % 2) - 1).abs());
final hueSegment = h ~/ 60;
var r = 0, g = 0, b = 0;
switch (hueSegment) {
case 0:
r = (255 * (c + m)).round();
g = (255 * (x + m)).round();
b = (255 * m).round();
break;
case 1:
r = (255 * (x + m)).round();
g = (255 * (c + m)).round();
b = (255 * m).round();
break;
case 2:
r = (255 * m).round();
g = (255 * (c + m)).round();
b = (255 * (x + m)).round();
break;
case 3:
r = (255 * m).round();
g = (255 * (x + m)).round();
b = (255 * (c + m)).round();
break;
case 4:
r = (255 * (x + m)).round();
g = (255 * m).round();
b = (255 * (c + m)).round();
break;
case 5:
default:
r = (255 * (c + m)).round();
g = (255 * m).round();
b = (255 * (x + m)).round();
break;
}
r = r.clamp(0, 255);
g = g.clamp(0, 255);
b = b.clamp(0, 255);
return Color.fromARGB(a, r, g, b);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment