Skip to content

Instantly share code, notes, and snippets.

@Katarn
Created December 30, 2016 09:52
Show Gist options
  • Save Katarn/588725dce850c81da8a551f92fb0923a to your computer and use it in GitHub Desktop.
Save Katarn/588725dce850c81da8a551f92fb0923a to your computer and use it in GitHub Desktop.
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'dart:convert';
/*
* TextView with HTML tags support By Kyle Katarn for Dart
*
* Original code by Erik Arvidsson, Mozilla Public License
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
* and ported it on JavaScript by John Resig (ejohn.org)
*/
import 'dart:ui';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'HtmlTextView Demo'),
);
}
}
// =====================================================================================================================
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
// =====================================================================================================================
class _MyHomePageState extends State<MyHomePage> {
Widget buildListItem(BuildContext context, String item) {
return new ListItem(
title: new HtmlTextView(data: item)
);
}
// =================================================================================================================
@override
Widget build(BuildContext context) {
String lorem = '<b>Lorem</b> <i>ipsum</i> <u>dolor</u> <span style="color: #FF0000">sit</span> ' +
'<span style="text-decoration: underline; font-weight: bold; font-style: italic">amet</span>, ' +
'<a hef="#">consectetur adipiscing elit</a>, sed do eiusmod tempor incididunt ut ' +
'labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation';
List<String> items = <String>[];
for (var i = 0; i < 100; i++) {
items.add(lorem);
}
Iterable<Widget> listItems = items.map((String item) => buildListItem(context, item));
return new Scaffold(
appBar: new AppBar(
title: new Text(config.title),
),
body: new Scrollbar(
child: new MaterialList(
padding: new EdgeInsets.symmetric(vertical: 4.0),
children: listItems
)
)
);
}
}
// =====================================================================================================================
class HtmlTextView extends StatelessWidget {
final String data;
// =================================================================================================================
HtmlTextView({
this.data
});
// =================================================================================================================
@override
Widget build(BuildContext context) {
HtmlParser parser = new HtmlParser();
List nodes = parser.parse(this.data);
TextSpan span = this._stackToTextSpan(nodes, context);
RichText contents = new RichText(text: span);
return new Container(
padding: const EdgeInsets.all(16.0),
child: contents
);
}
// =================================================================================================================
TextSpan _stackToTextSpan(List nodes, BuildContext context) {
List<TextSpan> children = <TextSpan>[];
for (int i = 0; i < nodes.length; i++) {
children.add(_textSpan(nodes[i]));
}
return new TextSpan(
text: '',
style: DefaultTextStyle.of(context).style,
children: children
);
}
// =================================================================================================================
TextSpan _textSpan(Map node) {
TextSpan span = new TextSpan(text: node['text'], style: node['style']);
return span;
}
}
// =====================================================================================================================
class HtmlParser {
// Regular Expressions for parsing tags and attributes
RegExp _startTag;
RegExp _endTag;
RegExp _attr;
RegExp _style;
RegExp _color;
final List _emptyTags = const ['area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', 'input',
'isindex', 'link', 'meta', 'param', 'embed'];
final List _blockTags = const ['address', 'applet', 'blockquote', 'button', 'center', 'dd', 'del', 'dir',
'div', 'dl', 'dt', 'fieldset', 'form', 'frameset', 'hr', 'iframe', 'ins',
'isindex', 'li', 'map', 'menu', 'noframes', 'noscript', 'object', 'ol',
'p', 'pre', 'script', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead',
'tr', 'ul'];
final List _inlineTags = const ['a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo', 'big', 'br', 'button',
'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'iframe', 'img', 'input',
'ins', 'kbd', 'label', 'map', 'object', 'q', 's', 'samp', 'script',
'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'textarea',
'tt', 'u', 'var'];
final List _closeSelfTags = const ['colgroup', 'dd', 'dt', 'li', 'options', 'p', 'td', 'tfoot', 'th', 'thead', 'tr'];
final List _fillAttrs = const ['checked', 'compact', 'declare', 'defer', 'disabled', 'ismap', 'multiple',
'nohref', 'noresize', 'noshade', 'nowrap', 'readonly', 'selected'];
final List _specialTags = const ['script', 'style'];
List _stack = [];
List _result = [];
Map<String, dynamic> _tag;
// =================================================================================================================
HtmlParser() {
this._startTag = new RegExp(r'^<([-A-Za-z0-9_]+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")' + "|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>");
this._endTag = new RegExp("^<\/([-A-Za-z0-9_]+)[^>]*>");
this._attr = new RegExp(r'([-A-Za-z0-9_]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")' + r"|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?");
this._style = new RegExp(r'([a-zA-Z\-]+)\s*:\s*([^;]*)');
this._color = new RegExp(r'^#([a-fA-F0-9]{6})$');
}
// =================================================================================================================
List parse(String html) {
String last = html;
Match match;
int index;
bool chars;
while (html.length > 0) {
chars = true;
// Make sure we're not in a script or style element
if (this._getStackLastItem() == null || !this._specialTags.contains(this._getStackLastItem())) {
// Comment
if (html.indexOf('<!--') == 0) {
index = html.indexOf('-->');
if (index >= 0) {
html = html.substring(index + 3);
chars = false;
}
}
// End tag
else if (html.indexOf('</') == 0) {
match = this._endTag.firstMatch(html);
if (match != null) {
String tag = match[0];
html = html.substring(tag.length);
chars = false;
this._parseEndTag(tag);
}
}
// Start tag
else if (html.indexOf('<') == 0) {
match = this._startTag.firstMatch(html);
if (match != null) {
String tag = match[0];
html = html.substring(tag.length);
chars = false;
this._parseStartTag(tag, match[1], match[2], match.start);
}
}
if (chars) {
index = html.indexOf('<');
String text = (index < 0) ? html : html.substring(0, index);
html = (index < 0) ? '' : html.substring(index);
this._appendNode(text);
}
}
else {
RegExp re = new RegExp(r'(.*)<\/' + this._getStackLastItem() + r'[^>]*>');
html = html.replaceAllMapped(re, (Match match) {
String text = match[0]
..replaceAll(new RegExp('<!--(.*?)-->'), '\$1')
..replaceAll(new RegExp('<!\[CDATA\[(.*?)]]>'), '\$1');
this._appendNode(text);
return '';
});
this._parseEndTag(this._getStackLastItem());
}
if (html == last) {
throw 'Parse Error: ' + html;
}
last = html;
}
// Cleanup any remaining tags
this._parseEndTag();
List result = this._result;
// Cleanup internal variables
this._stack = [];
this._result = [];
return result;
}
// =================================================================================================================
void _parseStartTag(String tag, String tagName, String rest, int unary) {
tagName = tagName.toLowerCase();
if (this._blockTags.contains(tagName)) {
while (this._getStackLastItem() != null && this._inlineTags.contains(this._getStackLastItem())) {
this._parseEndTag(this._getStackLastItem());
}
}
if (this._closeSelfTags.contains(tagName) && this._getStackLastItem() == tagName) {
this._parseEndTag(tagName);
}
if (this._emptyTags.contains(tagName)) {
unary = 1;
}
if (unary == 0) {
this._stack.add(tagName);
}
Map attrs = {};
Iterable<Match> matches = this._attr.allMatches(rest);
if (matches != null) {
for (Match match in matches) {
String attribute = match[1];
String value;
if (match[2] != null) {
value = match[2];
}
else if (match[3] != null) {
value = match[3];
}
else if (match[4] != null) {
value = match[4];
}
else if (this._fillAttrs.contains(attribute) != null) {
value = attribute;
}
attrs[attribute] = value;
}
}
this._appendTag(tagName, attrs);
}
// =================================================================================================================
void _parseEndTag([String tagName]) {
int pos;
// If no tag name is provided, clean shop
if (tagName == null) {
pos = 0;
}
// Find the closest opened tag of the same type
else {
for (pos = this._stack.length - 1; pos >= 0; pos--) {
if (this._stack[pos] == tagName) {
break;
}
}
}
if (pos >= 0) {
// Remove the open elements from the stack
this._stack.removeRange(pos, this._stack.length);
}
}
// =================================================================================================================
TextStyle _parseStyle(String tag, Map attrs) {
Iterable<Match> matches;
String style = attrs['style'];
String param;
String value;
Color color = new Color(0xFF000000);
FontWeight fontWeight = FontWeight.normal;
FontStyle fontStyle = FontStyle.normal;
TextDecoration textDecoration = TextDecoration.none;
switch (tag) {
case 'a':
color = new Color(int.parse('0xFF1965B5'));
break;
case 'b':
case 'strong':
fontWeight = FontWeight.bold;
break;
case 'i':
case 'em':
fontStyle = FontStyle.italic;
break;
case 'u':
textDecoration = TextDecoration.underline;
break;
}
if (style != null) {
matches = this._style.allMatches(style);
for (Match match in matches) {
param = match[1].trim();
value = match[2].trim();
switch (param) {
case 'color':
if (this._color.hasMatch(value)) {
value = value.replaceAll('#', '').trim();
color = new Color(int.parse('0xFF' + value));
}
break;
case 'font-weight':
fontWeight = (value == 'bold') ? FontWeight.bold : FontWeight.normal;
break;
case 'font-style':
fontStyle = (value == 'italic') ? FontStyle.italic : FontStyle.normal;
break;
case 'text-decoration':
textDecoration = (value == 'underline') ? TextDecoration.underline : TextDecoration.none;
break;
}
}
}
TextStyle textStyle = new TextStyle(
color: color,
fontWeight: fontWeight,
fontStyle: fontStyle,
decoration: textDecoration
);
return textStyle;
}
// =================================================================================================================
void _appendTag(String tag, Map attrs) {
this._tag = {
'tag': tag,
'attrs': attrs
};
}
// =================================================================================================================
void _appendNode(String text) {
if (this._tag == null) {
this._tag = {
'tag': 'p',
'attrs': {}
};
}
this._tag['text'] = text;
this._tag['style'] = this._parseStyle(this._tag['tag'], this._tag['attrs']);
this._tag['href'] = (this._tag['attrs']['href'] != null) ? this._tag['attrs']['href'] : '';
this._tag.remove('attrs');
this._result.add(this._tag);
this._tag = null;
}
// =================================================================================================================
String _getStackLastItem() {
if (this._stack.length <= 0) {
return null;
}
return this._stack[this._stack.length - 1];
}
}
// =====================================================================================================================
void main() {
runApp(new MyApp());
}
@krishnakumarcn
Copy link

krishnakumarcn commented Apr 22, 2020

I was looking at this example and tried to implement this. But I'm not able to get
tags on the list of nodes after parsing. Could you please guide me?
So if my HTML content is:
"<p>a</p><p><br></p><p><br></p><p>b</p><p><br></p><p><br></p><p>c</p><p>"

It'll only displays "abc"
Not
"a

b

c"

@Katarn
Copy link
Author

Katarn commented Apr 22, 2020

Could you please guide me?

This is a very old snippet, try this widgets:
https://pub.dev/packages/flutter_html (official Google implementation)
https://pub.dev/packages/flutter_widget_from_html

@krishnakumarcn
Copy link

krishnakumarcn commented Apr 23, 2020

I tried using the flutter_html but its not rendering as a Text-kind of widget and we don't have any control over the Text properties in there. We cant set the maxLines, etc. Here it is rendered as a RichText Widget, that's why this is a more suitable solution for me.

@Katarn
Copy link
Author

Katarn commented Apr 23, 2020

Unfortunately, I have not been involved in this script for a long time. You can try to remove br from _emptyTags, _inlineTags. Or add it to _blockTags. What exactly to change in the script logic to work as you need - now, unfortunately, I can not say.

@ali-thowfeek
Copy link

@krishnakumarcn you can try this package simple_html_css
@Katarn if you can, I would really appreciate a PR to the above package cos I'm struggling with rendering <ol> <ul> tags.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment